From cb7b7da0a4bb2b45f0b6092e42fd1ca385938f09 Mon Sep 17 00:00:00 2001 From: Cory Rivera Date: Wed, 25 Aug 2021 16:28:29 -0700 Subject: [PATCH] Merge from vscode e3c4990c67c40213af168300d1cfeb71d680f877 (#16569) --- .devcontainer/README.md | 57 +- .devcontainer/devcontainer.json | 18 +- .eslintrc.json | 15 +- .github/subscribers.json | 3 +- .github/workflows/ci.yml | 4 +- .vscode/notebooks/api.github-issues | 18 +- .vscode/notebooks/endgame.github-issues | 2 +- .vscode/notebooks/my-endgame.github-issues | 4 +- .vscode/notebooks/my-work.github-issues | 52 +- .vscode/tasks.json | 53 +- .yarnrc | 2 +- ThirdPartyNotices.txt | 231 ++ build/.cachesalt | 2 +- build/azure-pipelines/common/createAsset.js | 117 +- build/azure-pipelines/common/createAsset.ts | 121 +- build/azure-pipelines/common/createBuild.js | 2 +- .../common/extract-telemetry.sh | 12 +- .../azure-pipelines/common/publish-webview.js | 4 +- build/azure-pipelines/common/releaseBuild.js | 2 +- build/azure-pipelines/common/sync-mooncake.js | 87 - build/azure-pipelines/common/sync-mooncake.ts | 131 - .../darwin/product-build-darwin-sign.yml | 26 +- .../darwin/product-build-darwin.yml | 29 +- .../azure-pipelines/darwin/publish-server.sh | 14 - build/azure-pipelines/linux/alpine/publish.sh | 28 - .../linux/{publish.sh => prepare-publish.sh} | 11 +- .../linux/product-build-alpine.yml | 32 +- .../linux/product-build-linux.yml | 17 +- .../linux/snap-build-linux.yml | 8 +- build/azure-pipelines/product-build.yml | 51 +- build/azure-pipelines/product-compile.yml | 8 - build/azure-pipelines/product-publish.ps1 | 114 + build/azure-pipelines/product-publish.yml | 89 + .../{release.yml => product-release.yml} | 0 .../publish-types/update-types.js | 4 +- .../publish-types/update-types.ts | 4 +- build/azure-pipelines/sync-mooncake.yml | 24 - .../azure-pipelines/web/product-build-web.yml | 18 +- build/azure-pipelines/web/publish.sh | 15 - .../{publish.ps1 => prepare-publish.ps1} | 23 +- .../win32/product-build-win32.yml | 18 +- build/darwin/create-universal-app.js | 2 +- build/filters.js | 6 +- build/gulpfile.editor.js | 6 +- build/gulpfile.extensions.js | 156 +- build/gulpfile.vscode.js | 15 +- build/lib/builtInExtensions.js | 4 +- build/lib/builtInExtensions.ts | 4 +- build/lib/builtInExtensionsCG.js | 8 +- build/lib/builtInExtensionsCG.ts | 6 +- build/lib/compilation.js | 4 +- build/lib/compilation.ts | 2 +- build/lib/eslint/code-import-patterns.js | 4 +- build/lib/eslint/code-layering.js | 8 +- .../code-no-nls-in-standalone-editor.js | 4 +- build/lib/eslint/code-no-standalone-editor.js | 4 +- .../lib/eslint/code-no-unused-expressions.js | 216 +- build/lib/eslint/code-translation-remind.js | 4 +- .../eslint/vscode-dts-vscode-in-comments.js | 45 + .../eslint/vscode-dts-vscode-in-comments.ts | 53 + build/lib/extensions.js | 142 +- build/lib/extensions.ts | 143 +- build/lib/i18n.js | 95 +- build/lib/i18n.ts | 77 +- build/lib/layersChecker.js | 8 +- build/lib/locFunc.js | 2 +- build/lib/nls.js | 6 +- build/lib/optimize.js | 6 +- build/lib/optimize.ts | 2 +- build/lib/preLaunch.js | 2 +- build/lib/standalone.js | 2 +- build/lib/standalone.ts | 2 +- build/lib/treeshaking.js | 7 +- build/lib/treeshaking.ts | 10 +- build/lib/typings/gulp-bom.d.ts | 2 +- build/lib/typings/gulp-flatmap.d.ts | 4 +- build/lib/typings/vinyl.d.ts | 4 +- build/monaco/package.json | 4 +- build/npm/dirs.js | 49 +- build/npm/postinstall.js | 56 +- build/npm/update-localization-extension.js | 107 +- build/package.json | 6 +- build/tsconfig.json | 4 +- build/yarn.lock | 26 +- cglicenses.json | 47 +- cgmanifest.json | 4 +- extensions/azuremonitor/src/index.ts | 2 +- extensions/bat/cgmanifest.json | 4 +- .../bat/syntaxes/batchfile.tmLanguage.json | 72 +- extensions/configuration-editing/package.json | 2 +- .../schemas/attachContainer.schema.json | 42 +- .../devContainer.schema.generated.json | 440 ++- .../schemas/devContainer.schema.src.json | 86 +- .../src/settingsDocumentHelper.ts | 37 +- extensions/configuration-editing/yarn.lock | 8 +- extensions/dart/.vscodeignore | 2 + extensions/dart/cgmanifest.json | 46 + extensions/dart/language-configuration.json | 29 + extensions/dart/package.json | 35 + extensions/dart/package.nls.json | 4 + extensions/dart/syntaxes/dart.tmLanguage.json | 439 +++ extensions/extension-editing/package.json | 2 +- .../extension-editing/src/extensionLinter.ts | 7 +- extensions/extension-editing/yarn.lock | 8 +- extensions/git/package.json | 31 +- extensions/git/package.nls.json | 2 + extensions/git/src/commands.ts | 3 +- extensions/git/src/decorationProvider.ts | 1 - extensions/git/src/decorators.ts | 2 +- extensions/git/src/git.ts | 2 +- extensions/git/src/main.ts | 10 +- extensions/git/src/model.ts | 2 +- extensions/git/src/staging.ts | 2 +- extensions/git/src/test/git.test.ts | 72 +- extensions/git/src/test/smoke.test.ts | 24 +- extensions/git/src/util.ts | 2 +- extensions/git/yarn.lock | 324 +- extensions/github-authentication/package.json | 35 +- .../src/common/keychain.ts | 10 +- .../src/experimentationService.ts | 2 +- .../github-authentication/src/extension.ts | 79 +- .../github-authentication/src/github.ts | 132 +- .../github-authentication/src/githubServer.ts | 78 +- extensions/github-authentication/yarn.lock | 8 +- extensions/github/package.json | 2 +- extensions/github/yarn.lock | 10 +- .../src/binarySizeStatusBarEntry.ts | 7 +- .../image-preview/src/ownedStatusBarEntry.ts | 5 +- .../image-preview/src/sizeStatusBarEntry.ts | 7 +- .../image-preview/src/zoomStatusBarEntry.ts | 7 +- .../json-language-features/CONTRIBUTING.md | 12 +- .../client/src/jsonClient.ts | 21 +- .../json-language-features/package.json | 3 +- .../json-language-features/server/README.md | 10 +- .../server/package.json | 4 +- .../server/src/jsonServer.ts | 53 +- .../server/src/languageModelCache.ts | 2 +- .../json-language-features/server/yarn.lock | 16 +- extensions/json-language-features/yarn.lock | 8 +- extensions/json/build/update-grammars.js | 2 +- extensions/json/cgmanifest.json | 4 +- extensions/json/language-configuration.json | 7 +- extensions/json/package.json | 5 +- extensions/json/syntaxes/JSON.tmLanguage.json | 4 +- .../json/syntaxes/JSONC.tmLanguage.json | 4 +- extensions/julia/cgmanifest.json | 4 +- extensions/markdown-basics/cgmanifest.json | 2 +- .../language-configuration.json | 3 +- .../syntaxes/markdown.tmLanguage.json | 166 +- .../markdown-language-features/esbuild.js | 2 +- .../markdown-language-features/media/index.js | 1 - .../media/markdown.css | 8 + .../markdown-language-features/media/pre.js | 1 - .../notebook/index.ts | 180 +- .../markdown-language-features/package.json | 6 +- .../preview-src/activeLineMarker.ts | 2 +- .../preview-src/events.ts | 2 +- .../preview-src/index.ts | 2 - .../preview-src/loading.ts | 2 +- .../preview-src/pre.ts | 2 +- .../preview-src/tsconfig.json | 5 + .../src/commandManager.ts | 2 +- .../src/commands/index.ts | 14 +- .../src/commands/refreshPreview.ts | 2 +- .../src/commands/reloadPlugins.ts | 23 + .../commands/showPreviewSecuritySelector.ts | 2 +- .../src/commands/showSource.ts | 2 +- .../src/commands/toggleLock.ts | 2 +- .../src/extension.ts | 6 +- .../src/features/documentSymbolProvider.ts | 2 +- .../src/features/preview.ts | 57 +- .../src/features/previewConfig.ts | 17 +- .../src/features/previewContentProvider.ts | 2 +- .../src/features/previewManager.ts | 23 +- .../src/markdownEngine.ts | 143 +- .../src/test/engine.test.ts | 8 +- .../src/test/workspaceSymbolProvider.test.ts | 2 +- .../src/util/arrays.ts | 2 +- .../src/util/dispose.ts | 2 +- .../src/util/file.ts | 2 +- .../src/util/lazy.ts | 2 +- .../src/util/links.ts | 1 - .../src/util/resources.ts | 20 - .../src/util/topmostLineMonitor.ts | 25 +- .../markdown-language-features/yarn.lock | 13 +- .../.gitignore | 0 .../.vscodeignore | 0 .../README.md | 2 +- .../esbuild.js | 3 +- .../extension-browser.webpack.config.js | 17 + .../markdown-math/extension.webpack.config.js | 20 + .../icon.png | Bin extensions/markdown-math/notebook/katex.ts | 36 + .../notebook/tsconfig.json | 0 extensions/markdown-math/package.json | 69 + extensions/markdown-math/package.nls.json | 5 + .../preview-styles/index.css} | 9 +- extensions/markdown-math/src/extension.ts | 31 + extensions/markdown-math/src/types.d.ts | 1 + extensions/markdown-math/tsconfig.json | 17 + extensions/markdown-math/yarn.lock | 26 + extensions/merge-conflict/package.json | 2 +- .../merge-conflict/src/contentProvider.ts | 2 +- extensions/merge-conflict/src/delayer.ts | 2 +- extensions/merge-conflict/yarn.lock | 8 +- .../microsoft-authentication/package.json | 2 +- .../microsoft-authentication/src/AADHelper.ts | 52 +- .../src/microsoft-authentication.d.ts | 2 +- .../microsoft-authentication/tsconfig.json | 16 +- extensions/microsoft-authentication/yarn.lock | 8 +- .../notebook/katex.ts | 20 - .../notebook-markdown-extensions/package.json | 51 - .../package.nls.json | 4 - .../notebook-markdown-extensions/yarn.lock | 69 - extensions/package.json | 2 +- extensions/simple-browser/package.json | 5 +- .../simple-browser/preview-src/events.ts | 2 +- .../simple-browser/preview-src/index.ts | 1 - extensions/simple-browser/yarn.lock | 13 +- extensions/sql/cgmanifest.json | 4 +- extensions/sql/syntaxes/sql.tmLanguage.json | 6 +- .../theme-abyss/themes/abyss-color-theme.json | 2 +- extensions/theme-defaults/package.json | 14 +- .../theme-defaults/themes/dark_plus.json | 3 +- extensions/theme-defaults/themes/dark_vs.json | 2 +- .../theme-defaults/themes/hc_black.json | 3 +- .../theme-defaults/themes/light_plus.json | 4 +- .../theme-defaults/themes/light_vs.json | 5 +- .../themes/kimbie-dark-color-theme.json | 2 +- .../themes/dimmed-monokai-color-theme.json | 4 +- .../themes/monokai-color-theme.json | 5 +- .../themes/quietlight-color-theme.json | 2 +- .../theme-red/themes/Red-color-theme.json | 2 +- extensions/theme-seti/CONTRIBUTING.md | 29 +- .../theme-seti/build/update-icon-theme.js | 1 + extensions/theme-seti/cgmanifest.json | 2 +- extensions/theme-seti/icons/seti.woff | Bin 36472 -> 36672 bytes .../theme-seti/icons/vs-seti-icon-theme.json | 238 +- .../themes/solarized-dark-color-theme.json | 2 +- .../themes/solarized-light-color-theme.json | 34 +- .../tomorrow-night-blue-color-theme.json | 2 +- extensions/tsconfig.base.json | 4 +- .../test/colorize-fixtures/test.dart | 19 + .../test/colorize-results/test_bat.json | 8 +- .../test/colorize-results/test_cu.json | 40 +- .../test/colorize-results/test_dart.json | 673 ++++ .../vscode-notebook-tests/media/icon.png | Bin 2210 -> 0 bytes extensions/vscode-test-resolver/package.json | 10 +- .../vscode-test-resolver/src/extension.ts | 17 +- extensions/vscode-test-resolver/yarn.lock | 8 +- extensions/yarn.lock | 8 +- package.json | 41 +- product.json | 8 +- remote/package.json | 20 +- remote/web/package.json | 18 +- remote/web/yarn.lock | 40 +- remote/yarn.lock | 83 +- resources/darwin/code.icns | Bin 28204 -> 222366 bytes resources/linux/code-url-handler.desktop | 2 +- resources/linux/code.desktop | 4 +- resources/web/code-web.js | 3 - scripts/code.sh | 11 +- scripts/test-integration.bat | 2 +- scripts/test-integration.sh | 16 +- scripts/test.bat | 4 +- scripts/test.sh | 5 +- src/bootstrap-amd.js | 6 +- src/bootstrap-node.js | 1 + src/bootstrap-window.js | 29 +- src/main.js | 92 +- src/sql/base/browser/dom.ts | 2 +- .../base/browser/ui/buttonMenu/buttonMenu.ts | 4 +- .../ui/scrollableView/scrollableView.ts | 2 +- .../browser/ui/table/highPerf/cellCache.ts | 4 +- .../browser/ui/table/highPerf/tableView.ts | 2 +- .../browser/ui/table/highPerf/tableWidget.ts | 10 +- .../ui/table/plugins/headerFilter.plugin.ts | 6 +- .../connection/common/connectionConfig.ts | 14 +- .../common/connectionProfileGroup.ts | 3 +- .../common/connectionStatusManager.ts | 5 +- .../common/providerConnectionInfo.ts | 3 +- .../test/common/connectionProfile.test.ts | 3 +- .../test/common/connectionStore.test.ts | 22 +- .../common/providerConnectionInfo.test.ts | 5 +- .../test/node/connectionStatusManager.test.ts | 5 +- .../telemetry/common/adsTelemetryService.ts | 9 +- .../api/browser/mainThreadDataProtocol.ts | 3 +- .../api/browser/mainThreadModelViewDialog.ts | 5 +- .../workbench/api/common/extHostModelView.ts | 16 +- .../api/common/extHostModelViewDialog.ts | 2 +- .../api/common/extHostModelViewTree.ts | 3 +- .../browser/editData/editDataInput.ts | 3 +- .../browser/editData/editDataResultsInput.ts | 2 +- .../browser/editor/profiler/dashboardInput.ts | 2 +- .../browser/editor/profiler/profilerInput.ts | 2 +- .../resourceViewer/resourceViewerInput.ts | 2 +- .../workbench/browser/modal/dialogHelper.ts | 6 +- src/sql/workbench/browser/modal/modal.ts | 16 +- .../declarativeTable.component.ts | 9 +- .../modelComponents/diffeditor.component.ts | 6 +- .../groupContainer.component.ts | 3 +- .../modelComponents/image.component.ts | 3 +- .../modelComponents/inputbox.component.ts | 3 +- .../modelComponents/modelViewEditor.ts | 5 +- .../browser/modelComponents/modelViewInput.ts | 4 +- .../modelComponents/queryTextEditor.ts | 9 +- .../browser/modelComponents/text.component.ts | 9 +- .../modelComponents/treeComponentRenderer.ts | 2 +- .../browser/modelComponents/viewBase.ts | 3 +- .../common/editor/query/queryEditorInput.ts | 10 +- .../common/editor/query/queryResultsInput.ts | 2 +- .../editor/query/untitledQueryEditorInput.ts | 5 +- .../test/browser/asmtActions.test.ts | 3 +- .../contrib/charts/browser/actions.ts | 3 +- .../contrib/charts/browser/imageInsight.ts | 3 +- .../test/electron-browser/commandLine.test.ts | 6 +- .../connection/browser/connectionStatus.ts | 7 +- .../browser/core/dashboardPage.component.ts | 4 +- .../dashboard/browser/dashboardEditor.ts | 5 +- .../views/charts/chartInsight.component.ts | 116 +- .../views/charts/types/lineChart.component.ts | 27 +- .../charts/types/timeSeriesChart.component.ts | 4 +- .../insights/views/imageInsight.component.ts | 3 +- .../browser/connectionViewletPanel.ts | 2 +- .../browser/dataExplorerViewlet.ts | 4 +- .../editData/browser/editDataEditor.ts | 49 +- .../editData/browser/editDataGridPanel.ts | 4 +- .../editData/browser/editDataResultsEditor.ts | 5 +- .../browser/find/notebookFindDecorations.ts | 6 + .../browser/find/notebookFindWidget.ts | 12 +- .../browser/models/diffNotebookInput.ts | 4 +- .../browser/models/fileNotebookInput.ts | 2 +- .../notebook/browser/models/notebookInput.ts | 38 +- .../browser/models/notebookInputFactory.ts | 4 +- .../browser/models/untitledNotebookInput.ts | 5 +- .../notebook/browser/notebook.component.ts | 2 +- .../notebook/browser/notebook.contribution.ts | 6 +- .../browser/notebookEditor.component.ts | 2 +- .../notebook/browser/notebookEditor.ts | 5 +- .../notebookExplorerViewlet.ts | 4 +- .../notebookExplorer/notebookSearch.ts | 6 +- .../notebookViewsCodeCell.component.ts | 2 +- .../notebookViewsGrid.component.ts | 12 +- .../browser/outputs/gridOutput.component.ts | 3 +- .../browser/dataResourceDataProvider.test.ts | 17 +- .../browser/markdownTextTransformer.test.ts | 2 +- .../test/browser/notebookActions.test.ts | 2 +- .../test/browser/notebookEditor.test.ts | 10 +- .../test/browser/notebookInput.test.ts | 5 +- .../test/browser/notebookService.test.ts | 6 +- .../test/browser/notebookViewModel.test.ts | 4 +- .../test/browser/notebookViewsActions.test.ts | 10 +- .../browser/notebookViewsExtension.test.ts | 4 +- .../test/electron-browser/cell.test.ts | 19 +- .../electron-browser/clientSession.test.ts | 2 +- .../electron-browser/contentManagers.test.ts | 6 +- .../notebookEditorModel.test.ts | 8 +- .../notebookFindModel.test.ts | 6 +- .../electron-browser/notebookModel.test.ts | 20 +- .../test/browser/serverTreeView.test.ts | 2 +- .../profiler/browser/profilerEditor.ts | 41 +- .../profiler/browser/profilerFindWidget.ts | 12 +- .../browser/profilerResourceEditor.ts | 9 +- .../profiler/browser/profilerTableEditor.ts | 2 +- .../fileQueryEditorInput.ts | 2 +- .../contrib/query/browser/flavorStatus.ts | 6 +- .../contrib/query/browser/messagePanel.ts | 7 +- .../query/browser/query.contribution.ts | 8 +- .../contrib/query/browser/queryEditor.ts | 9 +- .../query/browser/queryInputFactory.ts | 6 +- .../query/browser/queryResultsEditor.ts | 5 +- .../contrib/query/browser/statusBarItems.ts | 17 +- .../query/test/browser/queryEditor.test.ts | 4 +- .../test/browser/queryInputFactory.test.ts | 17 +- .../browser/queryPlan.contribution.ts | 2 +- .../queryPlan/browser/queryPlanEditor.ts | 5 +- .../queryPlan/common/queryPlanInput.ts | 3 +- .../browser/resourceViewerEditor.ts | 9 +- .../browser/resourceViewerTable.ts | 4 +- .../contrib/views/browser/treeView.ts | 6 +- .../welcome/page/browser/welcomePage.ts | 10 +- .../connection/browser/connectionBrowseTab.ts | 4 +- .../browser/connectionController.ts | 3 +- .../browser/connectionManagementService.ts | 3 +- .../connection/browser/connectionWidget.ts | 3 +- .../connectionManagementService.test.ts | 61 +- .../connection/test/browser/testTreeView.ts | 2 +- .../electron-browser/insightsUtils.test.ts | 23 +- .../common/doHandleUpgrade.ts | 30 - .../common/languageAssociation.ts | 9 +- .../notebook/browser/models/notebookModel.ts | 3 +- .../notebook/browser/models/notebookUtils.ts | 6 +- .../browser/notebookViews/autodash.ts | 4 +- .../notebook/browser/sql/sqlSessionManager.ts | 3 +- .../browser/objectExplorerActions.ts | 4 +- .../browser/objectExplorerService.ts | 3 +- .../profiler/browser/profilerFilter.ts | 6 +- .../services/query/common/queryManagement.ts | 3 +- .../browser/editorDescriptorService.ts | 3 +- .../queryEditorService.test.ts | 12 +- .../editor/editorStatusModeSelect.test.ts | 13 +- .../test/browser/taskUtilities.test.ts | 3 +- .../api/extHostModelView.test.ts | 3 +- .../api/mainThreadNotebook.test.ts | 2 +- src/tsconfig.base.json | 4 +- src/tsec.exemptions.json | 1 + src/vs/base/browser/browser.ts | 3 +- src/vs/base/browser/dom.ts | 165 +- src/vs/base/browser/event.ts | 34 + src/vs/base/browser/globalMouseMoveMonitor.ts | 3 +- src/vs/base/browser/history.ts | 2 +- src/vs/base/browser/iframe.ts | 14 +- src/vs/base/browser/markdownRenderer.ts | 20 +- .../base/browser/ui/actionbar/actionbar.css | 10 + src/vs/base/browser/ui/aria/aria.css | 2 +- src/vs/base/browser/ui/button/button.css | 1 + src/vs/base/browser/ui/button/button.ts | 1 + .../browser/ui/centered/centeredViewLayout.ts | 1 + .../browser/ui/codicons/codicon/codicon.ttf | Bin 70964 -> 71056 bytes src/vs/base/browser/ui/dialog/dialog.ts | 38 +- src/vs/base/browser/ui/dropdown/dropdown.css | 7 + src/vs/base/browser/ui/dropdown/dropdown.ts | 4 +- .../base/browser/ui/findinput/findInput.css | 2 +- src/vs/base/browser/ui/findinput/findInput.ts | 10 +- .../base/browser/ui/findinput/replaceInput.ts | 10 +- src/vs/base/browser/ui/grid/grid.ts | 9 +- src/vs/base/browser/ui/grid/gridview.css | 2 +- src/vs/base/browser/ui/grid/gridview.ts | 69 +- src/vs/base/browser/ui/hover/hover.css | 5 + .../browser/ui/iconLabel/iconHoverDelegate.ts | 2 + src/vs/base/browser/ui/iconLabel/iconLabel.ts | 142 +- .../browser/ui/iconLabel/iconLabelHover.ts | 130 + .../base/browser/ui/iconLabel/iconlabel.css | 2 +- src/vs/base/browser/ui/list/listView.ts | 2 +- src/vs/base/browser/ui/list/listWidget.ts | 12 +- src/vs/base/browser/ui/list/splice.ts | 2 +- src/vs/base/browser/ui/sash/sash.ts | 192 +- .../browser/ui/selectBox/selectBoxCustom.ts | 2 +- src/vs/base/browser/ui/splitview/paneview.ts | 3 + src/vs/base/browser/ui/splitview/splitview.ts | 7 +- src/vs/base/browser/ui/table/table.css | 2 +- src/vs/base/browser/ui/table/table.ts | 2 +- src/vs/base/browser/ui/table/tableWidget.ts | 2 +- src/vs/base/browser/ui/tree/indexTreeModel.ts | 4 +- src/vs/base/common/actions.ts | 27 +- src/vs/base/common/arrays.ts | 109 +- src/vs/base/common/async.ts | 1 - src/vs/base/common/codicons.ts | 41 +- src/vs/base/common/collections.ts | 22 +- src/vs/base/common/color.ts | 2 +- src/vs/base/common/comparers.ts | 163 +- src/vs/base/common/decorators.ts | 79 +- src/vs/base/common/diff/diff.ts | 38 +- src/vs/base/common/event.ts | 27 +- src/vs/base/common/extpath.ts | 17 + src/vs/base/common/filters.ts | 2 +- src/vs/base/common/htmlContent.ts | 17 +- src/vs/base/common/idGenerator.ts | 2 +- src/vs/base/common/iterator.ts | 7 +- src/vs/base/common/jsonErrorMessages.ts | 2 +- src/vs/base/common/keybindingParser.ts | 2 +- src/vs/base/common/lifecycle.ts | 25 + src/vs/base/common/linkedList.ts | 8 + src/vs/base/common/map.ts | 33 +- src/vs/base/common/marked/marked.js | 2 +- src/vs/base/common/mime.ts | 17 + src/vs/base/common/network.ts | 1 + src/vs/base/common/objects.ts | 22 +- src/vs/base/common/parsers.ts | 2 +- src/vs/base/common/platform.ts | 2 +- src/vs/base/common/ports.ts | 13 + src/vs/base/common/process.ts | 4 +- src/vs/base/common/product.ts | 12 +- src/vs/base/common/resources.ts | 9 +- src/vs/base/common/strings.ts | 113 +- src/vs/base/common/uri.ts | 4 +- src/vs/base/common/uriIpc.ts | 2 +- src/vs/base/node/extpath.ts | 7 +- src/vs/base/node/pfs.ts | 151 +- src/vs/base/node/ports.ts | 9 - src/vs/base/node/powershell.ts | 4 +- src/vs/base/node/processes.ts | 5 +- src/vs/base/node/watcher.ts | 10 +- src/vs/base/node/zip.ts | 10 +- src/vs/base/parts/ipc/common/ipc.ts | 8 +- src/vs/base/parts/ipc/test/node/testApp.ts | 2 +- .../quickinput/browser/media/quickInput.css | 13 +- .../parts/quickinput/browser/quickInput.ts | 36 +- .../parts/quickinput/common/quickInput.ts | 15 +- .../parts/sandbox/common/electronTypes.ts | 8 +- .../base/parts/sandbox/common/sandboxTypes.ts | 6 +- .../parts/sandbox/electron-browser/preload.js | 6 - .../sandbox/electron-sandbox/electronTypes.ts | 32 +- src/vs/base/parts/storage/node/storage.ts | 11 +- .../parts/storage/test/node/storage.test.ts | 31 +- .../base/parts/tree/browser/treeDefaults.ts | 2 +- src/vs/base/parts/tree/browser/treeView.ts | 14 +- src/vs/base/test/browser/comparers.test.ts | 472 ++- src/vs/base/test/common/arrays.test.ts | 52 +- src/vs/base/test/common/async.test.ts | 2 +- src/vs/base/test/common/codicon.test.ts | 82 +- src/vs/base/test/common/codicons.test.ts | 2 +- src/vs/base/test/common/collections.test.ts | 22 - src/vs/base/test/common/decorators.test.ts | 26 +- src/vs/base/test/common/event.test.ts | 68 +- .../base/test/common/filters.perf.data.d.ts | 2 +- src/vs/base/test/common/filters.perf.data.js | 2 +- src/vs/base/test/common/glob.test.ts | 6 + src/vs/base/test/common/map.test.ts | 6 +- src/vs/base/test/common/mime.test.ts | 11 +- src/vs/base/test/common/processes.test.ts | 2 +- src/vs/base/test/common/stream.test.ts | 2 +- src/vs/base/test/common/testUtils.ts | 19 + src/vs/base/test/node/crypto.test.ts | 9 +- src/vs/base/test/node/extpath.test.ts | 7 +- src/vs/base/test/node/pfs/fixtures/site.css | 4 +- src/vs/base/test/node/pfs/pfs.test.ts | 104 +- .../base/test/node/processes/fixtures/fork.ts | 2 +- .../node/processes/fixtures/fork_large.ts | 2 +- src/vs/base/test/node/testUtils.ts | 17 +- src/vs/base/test/node/zip/zip.test.ts | 9 +- .../quickinput/browser/quickinput.test.ts | 75 + src/vs/code/browser/workbench/workbench.ts | 4 +- .../sharedProcess/contrib/codeCacheCleaner.ts | 68 + .../contrib/languagePackCachedDataCleaner.ts | 129 +- .../sharedProcess/contrib/logsDataCleaner.ts | 44 +- .../contrib/nodeCachedDataCleaner.ts | 87 - .../contrib/storageDataCleaner.ts | 71 +- .../sharedProcess/sharedProcess.js | 5 +- .../sharedProcess/sharedProcessMain.ts | 20 +- .../electron-browser/workbench/workbench.html | 2 +- .../electron-browser/workbench/workbench.js | 42 +- src/vs/code/electron-main/app.ts | 282 +- src/vs/code/electron-main/auth.ts | 1 - src/vs/code/electron-main/main.ts | 58 +- .../electron-sandbox/issue/issueReporter.js | 5 +- .../issue/issueReporterMain.ts | 22 +- .../issue/issueReporterModel.ts | 2 + .../issue/test/testReporterModel.test.ts | 6 + .../processExplorer/media/processExplorer.css | 4 + .../processExplorer/processExplorer.js | 5 +- .../processExplorer/processExplorerMain.ts | 64 +- .../electron-sandbox/workbench/workbench.html | 2 +- .../electron-sandbox/workbench/workbench.js | 102 +- src/vs/code/node/cli.ts | 11 +- src/vs/code/node/cliProcessMain.ts | 27 +- .../editor/browser/controller/mouseTarget.ts | 202 +- .../editor/browser/core/markdownRenderer.ts | 6 +- src/vs/editor/browser/editorBrowser.ts | 10 +- src/vs/editor/browser/editorExtensions.ts | 69 +- .../services/abstractCodeEditorService.ts | 2 +- .../browser/services/codeEditorService.ts | 6 +- .../browser/services/codeEditorServiceImpl.ts | 10 +- .../browser/services/markerDecorations.ts | 2 +- .../editor/browser/services/openerService.ts | 30 +- .../viewParts/decorations/decorations.css | 2 +- .../browser/viewParts/lines/viewLine.ts | 37 +- .../linesDecorations/linesDecorations.css | 2 +- .../marginDecorations/marginDecorations.css | 2 +- .../marginDecorations/marginDecorations.ts | 2 +- .../browser/viewParts/minimap/minimap.ts | 2 +- .../overlayWidgets/overlayWidgets.css | 2 +- .../browser/viewParts/rulers/rulers.css | 2 +- .../scrollDecoration/scrollDecoration.css | 2 +- .../editor/browser/widget/codeEditorWidget.ts | 42 +- .../editor/browser/widget/diffEditorWidget.ts | 12 +- src/vs/editor/browser/widget/diffReview.ts | 15 +- src/vs/editor/common/commands/shiftCommand.ts | 3 + .../common/config/commonEditorConfig.ts | 1 + src/vs/editor/common/config/editorOptions.ts | 117 +- src/vs/editor/common/controller/cursor.ts | 2 + .../editor/common/controller/cursorCommon.ts | 67 +- .../controller/cursorDeleteOperations.ts | 94 +- .../common/controller/cursorMoveCommands.ts | 56 +- .../common/controller/cursorMoveOperations.ts | 102 +- .../common/controller/cursorTypeOperations.ts | 70 +- src/vs/editor/common/core/lineTokens.ts | 16 +- src/vs/editor/common/diff/diffComputer.ts | 4 + src/vs/editor/common/editorCommon.ts | 3 +- src/vs/editor/common/editorContextKeys.ts | 2 +- src/vs/editor/common/model.ts | 34 +- src/vs/editor/common/model/textModel.ts | 36 +- src/vs/editor/common/modes.ts | 121 +- src/vs/editor/common/modes/linkComputer.ts | 2 +- .../editor/common/modes/supports/onEnter.ts | 7 +- .../services/markerDecorationsServiceImpl.ts | 1 + .../common/standalone/standaloneEnums.ts | 174 +- .../editor/common/view/editorColorRegistry.ts | 3 + .../common/viewLayout/viewLineRenderer.ts | 76 +- .../common/viewModel/splitLinesCollection.ts | 72 +- .../editor/common/viewModel/viewModelImpl.ts | 14 +- .../contrib/anchorSelect/anchorSelect.ts | 1 + .../bracketMatching/bracketMatching.ts | 2 + .../contrib/caretOperations/transpose.ts | 4 +- src/vs/editor/contrib/clipboard/clipboard.ts | 17 + .../contrib/codeAction/codeActionMenu.ts | 5 +- .../contrib/codelens/codelensController.ts | 2 +- .../contrib/colorPicker/colorDetector.ts | 2 +- src/vs/editor/contrib/comment/comment.ts | 2 +- .../editor/contrib/contextmenu/contextmenu.ts | 8 +- src/vs/editor/contrib/dnd/dnd.css | 2 +- src/vs/editor/contrib/dnd/dnd.ts | 1 + src/vs/editor/contrib/find/findController.ts | 67 +- src/vs/editor/contrib/find/findDecorations.ts | 6 + .../contrib/find/test/findController.test.ts | 4 +- src/vs/editor/contrib/folding/folding.ts | 2 +- .../contrib/folding/foldingDecorations.ts | 5 + .../folding/intializingRangeProvider.ts | 1 + .../contrib/folding/test/foldingModel.test.ts | 3 + .../editor/contrib/gotoSymbol/goToCommands.ts | 2 +- .../link/goToDefinitionAtPosition.css | 2 +- .../link/goToDefinitionAtPosition.ts | 1 + .../gotoSymbol/peek/referencesWidget.ts | 1 + .../contrib/hover/colorHoverParticipant.ts | 153 + src/vs/editor/contrib/hover/hover.ts | 78 +- src/vs/editor/contrib/hover/hoverTypes.ts | 87 + .../contrib/hover/markdownHoverParticipant.ts | 54 +- .../contrib/hover/markerHoverParticipant.ts | 53 +- .../editor/contrib/hover/modesContentHover.ts | 487 ++- .../contrib/inPlaceReplace/inPlaceReplace.ts | 1 + .../editor/contrib/indentation/indentUtils.ts | 2 +- .../inlayHintsController.ts} | 82 +- .../contrib/inlineCompletions/ghostText.css | 20 + .../inlineCompletions/ghostTextController.ts | 343 +++ .../inlineCompletions/ghostTextWidget.ts | 427 +++ .../inlineCompletionsHoverParticipant.ts | 114 + .../inlineCompletionsModel.ts | 592 ++++ .../suggestWidgetAdapterModel.ts | 187 ++ .../editor/contrib/inlineCompletions/utils.ts | 51 + .../linesOperations/linesOperations.ts | 49 +- .../contrib/linkedEditing/linkedEditing.ts | 1 + src/vs/editor/contrib/links/links.ts | 2 + .../editor/contrib/multicursor/multicursor.ts | 2 + .../contrib/peekView/media/peekViewWidget.css | 1 + src/vs/editor/contrib/peekView/peekView.ts | 2 +- .../quickAccess/commandsQuickAccess.ts | 6 +- .../editorNavigationQuickAccess.ts | 2 + .../contrib/snippet/snippetController2.ts | 2 +- .../editor/contrib/snippet/snippetParser.ts | 4 +- .../editor/contrib/snippet/snippetSession.ts | 8 +- .../snippet/test/snippetParser.test.ts | 1 + .../editor/contrib/suggest/media/suggest.css | 4 + src/vs/editor/contrib/suggest/resizable.ts | 2 + src/vs/editor/contrib/suggest/suggest.ts | 5 + .../contrib/suggest/suggestController.ts | 10 +- src/vs/editor/contrib/suggest/suggestModel.ts | 8 +- .../editor/contrib/suggest/suggestWidget.ts | 29 +- .../contrib/suggest/suggestWidgetDetails.ts | 1 + .../contrib/suggest/suggestWidgetRenderer.ts | 12 +- .../suggest/test/completionModel.test.ts | 1 + .../wordHighlighter/wordHighlighter.ts | 3 + .../editor/contrib/zoneWidget/zoneWidget.ts | 2 +- src/vs/editor/editor.all.ts | 3 +- .../accessibilityHelp/accessibilityHelp.css | 2 +- .../iPadShowKeyboard/iPadShowKeyboard.css | 2 +- .../iPadShowKeyboard/iPadShowKeyboard.ts | 4 +- .../standaloneCommandsQuickAccess.ts | 6 +- .../quickInput/standaloneQuickInput.css | 5 + .../browser/standaloneCodeEditor.ts | 15 + .../browser/standaloneCodeServiceImpl.ts | 12 +- .../standalone/browser/standaloneLanguages.ts | 11 +- .../browser/standaloneThemeServiceImpl.ts | 2 +- .../common/monarch/monarchCompile.ts | 13 +- src/vs/editor/standalone/common/themes.ts | 5 +- .../standalone/test/monarch/monarch.test.ts | 27 + .../test/browser/controller/cursor.test.ts | 261 ++ .../editor/test/browser/editorTestServices.ts | 4 +- .../services/decorationRenderOptions.test.ts | 22 +- .../browser/services/openerService.test.ts | 87 +- .../test/common/core/stringBuilder.test.ts | 2 +- .../test/common/diff/diffComputer.test.ts | 51 + .../test/common/model/benchmark/bootstrap.js | 2 +- .../test/common/model/benchmark/entry.ts | 2 +- .../test/common/model/editStack.test.ts | 2 +- .../common/model/modelDecorations.test.ts | 38 +- .../test/common/modes/linkComputer.test.ts | 21 + .../common/modes/supports/onEnter.test.ts | 34 + .../viewLayout/viewLineRenderer.test.ts | 346 ++- .../viewModel/viewModelDecorations.test.ts | 3 + src/vs/monaco.d.ts | 319 +- .../dropdownWithPrimaryActionViewItem.ts | 65 +- .../browser/menuEntryActionViewItem.ts | 30 +- src/vs/platform/actions/common/actions.ts | 46 +- src/vs/platform/actions/common/menuService.ts | 4 +- .../backup/electron-main/backupMainService.ts | 33 +- .../electron-main/backupMainService.test.ts | 48 +- .../checksum/common/checksumService.ts | 2 +- .../platform/checksum/node/checksumService.ts | 2 +- .../test/node/checksumService.test.ts | 2 +- .../configuration/common/configuration.ts | 2 +- .../platform/contextkey/common/contextkey.ts | 1 - .../platform/contextkey/common/contextkeys.ts | 3 +- .../test/browser/contextkey.test.ts | 53 + .../contextkey/test/common/contextkey.test.ts | 2 +- .../contextview/browser/contextMenuService.ts | 3 + .../contextview/browser/contextView.ts | 3 + .../electron-sandbox/diagnosticsService.ts | 2 +- .../diagnostics/node/diagnosticsService.ts | 69 +- src/vs/platform/dialogs/common/dialogs.ts | 9 +- .../electron-main/dialogMainService.ts | 15 +- .../dialogs/test/common/testDialogService.ts | 23 +- src/vs/platform/driver/common/driverIpc.ts | 2 +- src/vs/platform/editor/common/editor.ts | 102 +- src/vs/platform/environment/common/argv.ts | 1 + .../environment/common/environment.ts | 3 + .../environment/common/environmentService.ts | 5 +- .../electron-main/environmentMainService.ts | 12 +- src/vs/platform/environment/node/argv.ts | 9 +- .../platform/environment/node/userDataPath.js | 2 +- .../test/node/userDataPath.test.ts | 2 +- .../common/extensionGalleryService.ts | 205 +- .../common/extensionManagement.ts | 3 +- .../common/extensionManagementIpc.ts | 8 +- .../node/extensionDownloader.ts | 4 +- .../node/extensionLifecycle.ts | 4 +- .../node/extensionManagementService.ts | 26 +- .../node/extensionManagementUtil.ts | 2 +- .../node/extensionsManifestCache.ts | 2 +- .../node/extensionsScanner.ts | 29 +- .../node/extensionsWatcher.ts | 2 +- .../extensions/common/extensionValidator.ts | 53 +- .../platform/extensions/common/extensions.ts | 32 +- .../test/common/extensionValidator.test.ts | 22 +- .../common/externalTerminal.ts | 19 +- .../externalTerminalService.test.ts | 4 +- .../externalTerminalMainService.ts | 16 + .../node/externalTerminalService.ts | 178 +- .../files/browser/htmlFileSystemProvider.ts | 2 +- .../browser/indexedDBFileSystemProvider.ts | 55 +- src/vs/platform/files/common/fileService.ts | 37 +- src/vs/platform/files/common/files.ts | 68 +- .../files/common/ipcFileSystemProvider.ts | 2 +- .../diskFileSystemProvider.ts | 13 +- .../files/node/diskFileSystemProvider.ts | 41 +- .../test/browser/indexedDBFileService.test.ts | 6 +- .../platform/files/test/common/files.test.ts | 4 - .../electron-browser/diskFileService.test.ts | 72 +- .../fixtures/resolver/site.css | 4 +- .../electron-browser/mainProcessService.ts | 2 +- .../platform/ipc/electron-sandbox/services.ts | 2 +- src/vs/platform/issue/common/issue.ts | 11 +- .../issue/electron-main/issueMainService.ts | 35 +- .../common/jsonContributionRegistry.ts | 2 +- .../common/abstractKeybindingService.ts | 11 +- .../platform/keybinding/common/keybinding.ts | 4 +- .../keybinding/common/keybindingResolver.ts | 12 +- .../test/common/keybindingResolver.test.ts | 19 +- .../keyboardLayout/common/dispatchConfig.ts | 2 +- .../electron-main/lifecycleMainService.ts | 106 +- src/vs/platform/list/browser/listService.ts | 86 +- .../localizations/node/localizations.ts | 7 +- src/vs/platform/log/node/spdlogLog.ts | 13 +- .../platform/menubar/electron-main/menubar.ts | 96 +- src/vs/platform/native/common/native.ts | 10 +- .../electron-main/nativeHostMainService.ts | 144 +- .../native/electron-sandbox/native.ts | 2 +- src/vs/platform/opener/browser/link.ts | 100 +- src/vs/platform/opener/common/opener.ts | 1 + src/vs/platform/product/common/product.ts | 8 +- src/vs/platform/progress/common/progress.ts | 10 +- .../protocol/electron-main/protocol.ts | 8 +- .../electron-main/protocolMainService.ts | 11 +- .../quickinput/browser/commandsQuickAccess.ts | 9 +- .../quickinput/browser/pickerQuickAccess.ts | 8 +- .../quickinput/browser/quickAccess.ts | 30 + .../platform/quickinput/browser/quickInput.ts | 4 +- .../platform/quickinput/common/quickAccess.ts | 7 + .../browser/remoteAuthorityResolverService.ts | 7 + .../remote/common/remoteAuthorityResolver.ts | 18 +- src/vs/platform/remote/common/remoteHosts.ts | 6 +- src/vs/platform/remote/common/tunnel.ts | 5 + .../remoteAuthorityResolverService.ts | 70 +- src/vs/platform/remote/node/tunnelService.ts | 31 +- .../electron-main/sharedProcess.ts | 15 +- .../state/{node => electron-main}/state.ts | 8 +- .../state/electron-main/stateMainService.ts | 189 ++ src/vs/platform/state/node/stateService.ts | 158 - .../state/test/electron-main/state.test.ts | 202 ++ src/vs/platform/state/test/node/state.test.ts | 57 - .../storage/browser/storageService.ts | 361 ++- src/vs/platform/storage/common/storageIpc.ts | 2 +- .../storage/electron-main/storageIpc.ts | 2 +- .../storage/electron-main/storageMain.ts | 13 +- .../electron-main/storageMainService.ts | 2 +- .../electron-sandbox/storageService.ts | 2 +- .../test/browser/storageService.test.ts | 204 +- .../electron-main/storageMainService.test.ts | 6 +- .../telemetry/common/commonProperties.ts | 2 +- .../customEndpointTelemetryService.ts | 2 +- .../node/customEndpointTelemetryService.ts | 2 +- src/vs/platform/telemetry/node/telemetry.ts | 47 +- .../test/browser/telemetryService.test.ts | 48 +- .../terminal/common/environmentVariable.ts | 2 +- src/vs/platform/terminal/common/terminal.ts | 193 +- .../terminal/common/terminalEnvironment.ts | 14 + .../common/terminalPlatformConfiguration.ts | 394 +++ .../terminal/common/terminalProcess.ts | 9 +- .../terminal/common/terminalRecorder.ts | 8 +- .../terminal/electron-sandbox/terminal.ts | 2 +- .../terminal/node/heartbeatService.ts | 2 +- src/vs/platform/terminal/node/ptyHostMain.ts | 8 +- .../platform/terminal/node/ptyHostService.ts | 57 +- src/vs/platform/terminal/node/ptyService.ts | 90 +- .../terminal/node/terminalEnvironment.ts | 4 +- .../platform/terminal/node/terminalProcess.ts | 87 +- .../terminal/node/terminalProfiles.ts | 362 +++ .../terminal/node/windowsShellHelper.ts | 16 +- .../test/common/terminalRecorder.test.ts | 2 +- src/vs/platform/theme/common/colorRegistry.ts | 137 +- src/vs/platform/theme/common/styler.ts | 22 +- src/vs/platform/theme/common/themeService.ts | 4 + .../theme/electron-main/themeMainService.ts | 56 +- .../undoRedo/common/undoRedoService.ts | 2 +- src/vs/platform/update/common/updateIpc.ts | 2 +- .../electron-main/abstractUpdateService.ts | 2 +- .../electron-main/updateService.snap.ts | 2 +- .../electron-main/updateService.win32.ts | 14 +- .../common/abstractSynchronizer.ts | 25 +- .../userDataSync/common/extensionsMerge.ts | 31 +- .../userDataSync/common/extensionsSync.ts | 49 +- .../userDataSync/common/globalStateSync.ts | 5 +- .../userDataSync/common/ignoredExtensions.ts | 2 +- .../userDataSync/common/keybindingsSync.ts | 5 +- .../userDataSync/common/settingsMerge.ts | 4 +- .../userDataSync/common/settingsSync.ts | 5 +- .../userDataSync/common/snippetsSync.ts | 5 +- .../common/userDataAutoSyncService.ts | 2 +- .../userDataSync/common/userDataSync.ts | 6 +- .../test/common/extensionsMerge.test.ts | 208 +- .../test/common/synchronizer.test.ts | 2 +- src/vs/platform/windows/common/windows.ts | 30 +- .../platform/windows/electron-main/window.ts | 53 +- .../windows/electron-main/windowsFinder.ts | 2 +- .../electron-main/windowsMainService.ts | 12 +- .../electron-main/windowsStateHandler.ts | 28 +- .../test/electron-main/windowsFinder.test.ts | 2 +- .../electron-main/windowsStateHandler.test.ts | 201 ++ .../workspace/common/workspaceTrust.ts | 47 +- .../platform/workspaces/common/workspaces.ts | 18 +- .../workspacesHistoryMainService.ts | 14 +- .../electron-main/workspacesMainService.ts | 4 +- .../workspacesManagementMainService.ts | 36 +- .../workspacesManagementMainService.test.ts | 4 +- src/vs/vscode.d.ts | 2709 +++++++++++------ src/vs/vscode.proposed.d.ts | 1527 ++++------ src/vs/workbench/api/browser/apiCommands.ts | 2 +- .../api/browser/extensionHost.contribution.ts | 1 + .../api/browser/mainThreadAuthentication.ts | 8 +- .../api/browser/mainThreadCLICommands.ts | 2 +- .../api/browser/mainThreadCustomEditors.ts | 161 +- .../api/browser/mainThreadDebugService.ts | 3 +- .../api/browser/mainThreadDocuments.ts | 73 +- .../browser/mainThreadDocumentsAndEditors.ts | 5 +- .../api/browser/mainThreadDownloadService.ts | 2 +- .../workbench/api/browser/mainThreadEditor.ts | 2 +- .../api/browser/mainThreadEditorTabs.ts | 10 +- .../api/browser/mainThreadEditors.ts | 5 +- .../api/browser/mainThreadExtensionService.ts | 41 +- .../api/browser/mainThreadFileSystem.ts | 10 +- .../api/browser/mainThreadLabelService.ts | 2 +- .../api/browser/mainThreadLanguageFeatures.ts | 33 +- .../api/browser/mainThreadNotebook.ts | 33 +- .../browser/mainThreadNotebookDocuments.ts | 60 +- .../mainThreadNotebookDocumentsAndEditors.ts | 11 +- .../api/browser/mainThreadNotebookEditors.ts | 26 +- .../api/browser/mainThreadNotebookKernels.ts | 25 +- .../browser/mainThreadNotebookRenderers.ts | 29 + .../api/browser/mainThreadStatusBar.ts | 10 +- .../workbench/api/browser/mainThreadTask.ts | 3 - .../api/browser/mainThreadTerminalService.ts | 107 +- .../api/browser/mainThreadTreeViews.ts | 4 +- .../api/browser/mainThreadTunnelService.ts | 3 +- .../api/browser/mainThreadWebviews.ts | 5 +- .../workbench/api/browser/mainThreadWindow.ts | 3 +- src/vs/workbench/api/common/apiCommands.ts | 96 - .../workbench/api/common/extHost.api.impl.ts | 209 +- .../api/common/extHost.common.services.ts | 6 +- .../workbench/api/common/extHost.protocol.ts | 174 +- .../api/common/extHostApiCommands.ts | 64 +- .../api/common/extHostAuthentication.ts | 8 +- .../workbench/api/common/extHostCodeInsets.ts | 8 +- .../workbench/api/common/extHostCommands.ts | 8 +- .../api/common/extHostDebugService.ts | 56 +- .../api/common/extHostDiagnostics.ts | 22 +- .../workbench/api/common/extHostEditorTabs.ts | 17 +- .../api/common/extHostExtensionService.ts | 43 +- .../workbench/api/common/extHostFileSystem.ts | 17 +- .../api/common/extHostLabelService.ts | 2 +- .../api/common/extHostLanguageFeatures.ts | 218 +- src/vs/workbench/api/common/extHostMemento.ts | 5 + .../workbench/api/common/extHostNotebook.ts | 442 +-- .../api/common/extHostNotebookDocument.ts | 92 +- .../api/common/extHostNotebookDocuments.ts | 50 + .../api/common/extHostNotebookEditor.ts | 20 +- .../api/common/extHostNotebookEditors.ts | 88 + .../api/common/extHostNotebookKernels.ts | 373 ++- .../api/common/extHostNotebookRenderers.ts | 59 + .../workbench/api/common/extHostStatusBar.ts | 75 +- src/vs/workbench/api/common/extHostTask.ts | 6 - .../workbench/api/common/extHostTelemetry.ts | 2 +- .../api/common/extHostTerminalService.ts | 157 +- src/vs/workbench/api/common/extHostTesting.ts | 2 +- .../api/common/extHostTestingPrivateApi.ts | 2 +- .../workbench/api/common/extHostTextEditor.ts | 5 +- .../api/common/extHostTextEditors.ts | 5 +- .../api/common/extHostTypeConverters.ts | 151 +- src/vs/workbench/api/common/extHostTypes.ts | 321 +- src/vs/workbench/api/common/extHostWebview.ts | 47 +- .../api/common/extHostWebviewMessaging.ts | 6 +- src/vs/workbench/api/common/extHostWindow.ts | 2 - .../workbench/api/common/extHostWorkspace.ts | 3 +- .../api/common/menusExtensionPoint.ts | 31 +- src/vs/workbench/api/common/shared/tasks.ts | 1 + .../api/common/shared/treeDataTransfer.ts | 2 +- src/vs/workbench/api/common/shared/webview.ts | 63 +- .../api/node/extHost.node.services.ts | 6 +- .../workbench/api/node/extHostDebugService.ts | 34 +- .../api/node/extHostOutputService.ts | 21 +- src/vs/workbench/api/node/extHostTask.ts | 13 +- .../api/node/extHostTerminalService.ts | 126 +- .../api/node/extHostTunnelService.ts | 13 +- .../browser/{menuActions.ts => actions.ts} | 8 + .../browser/actions/developerActions.ts | 30 +- .../workbench/browser/actions/helpActions.ts | 251 +- .../browser/actions/layoutActions.ts | 681 +++-- .../browser/actions/windowActions.ts | 350 +-- .../browser/actions/workspaceActions.ts | 196 +- .../browser/actions/workspaceCommands.ts | 95 +- src/vs/workbench/browser/codeeditor.ts | 2 + src/vs/workbench/browser/composite.ts | 11 +- src/vs/workbench/browser/contextkeys.ts | 13 +- src/vs/workbench/browser/dnd.ts | 457 +-- src/vs/workbench/browser/editor.ts | 176 +- src/vs/workbench/browser/labels.ts | 2 +- src/vs/workbench/browser/layout.ts | 52 +- src/vs/workbench/browser/media/style.css | 2 + src/vs/workbench/browser/panel.ts | 2 +- .../parts/activitybar/activitybarPart.ts | 29 +- .../browser/parts/banner/bannerPart.ts | 331 ++ .../browser/parts/banner/media/bannerpart.css | 52 + .../workbench/browser/parts/compositeBar.ts | 5 +- .../browser/parts/dialogs/dialogHandler.ts | 4 +- .../browser/parts/editor/binaryEditor.ts | 10 +- .../parts/editor/breadcrumbsControl.ts | 6 +- .../parts/editor/editor.contribution.ts | 245 +- .../workbench/browser/parts/editor/editor.ts | 16 +- .../browser/parts/editor/editorActions.ts | 28 +- .../browser/parts/editor/editorAutoSave.ts | 4 +- .../browser/parts/editor/editorCommands.ts | 32 +- .../browser/parts/editor/editorControl.ts | 56 +- .../browser/parts/editor/editorDropTarget.ts | 40 +- .../browser/parts/editor/editorGroupView.ts | 101 +- .../browser/parts/editor/editorPane.ts | 18 +- .../browser/parts/editor/editorPart.ts | 6 - .../browser/parts/editor/editorQuickAccess.ts | 2 +- .../browser/parts/editor/editorStatus.ts | 46 +- .../browser/parts/editor/editorsObserver.ts | 3 +- .../browser/parts/editor/media/back-tb.png | Bin 266 -> 254 bytes .../browser/parts/editor/media/forward-tb.png | Bin 258 -> 267 bytes .../editor/media/workspacetrusteditor.css | 20 + .../parts/editor/noTabsTitleControl.ts | 8 +- .../browser/parts/editor/rangeDecorations.ts | 121 - .../browser/parts/editor/sideBySideEditor.ts | 54 +- .../browser/parts/editor/tabsTitleControl.ts | 18 +- .../browser/parts/editor/textDiffEditor.ts | 140 +- .../browser/parts/editor/textEditor.ts | 39 +- .../parts/editor/textResourceEditor.ts | 28 +- .../browser/parts/editor/titleControl.ts | 63 +- .../editor/workspaceTrustRequiredEditor.ts | 129 + .../browser/parts/media/compositepart.css | 2 +- .../notifications/notificationsActions.ts | 2 +- .../notifications/notificationsStatus.ts | 9 +- .../notifications/notificationsTelemetry.ts | 2 +- .../notifications/notificationsViewer.ts | 16 +- .../browser/parts/sidebar/sidebarPart.ts | 8 +- .../browser/parts/statusbar/statusbarPart.ts | 141 +- .../browser/parts/titlebar/menubarControl.ts | 170 +- .../browser/parts/titlebar/titlebarPart.ts | 1 + .../browser/parts/views/media/views.css | 9 +- .../workbench/browser/parts/views/viewPane.ts | 13 +- .../browser/parts/views/viewPaneContainer.ts | 12 +- src/vs/workbench/browser/style.ts | 12 +- src/vs/workbench/browser/web.main.ts | 17 +- src/vs/workbench/browser/window.ts | 40 +- src/vs/workbench/browser/workbench.ts | 17 +- src/vs/workbench/common/editor.ts | 813 +---- .../common/editor/binaryEditorModel.ts | 12 +- .../common/editor/diffEditorInput.ts | 43 +- .../common/editor/diffEditorModel.ts | 2 +- .../common/editor/editorGroupModel.ts | 20 +- src/vs/workbench/common/editor/editorInput.ts | 140 + src/vs/workbench/common/editor/editorModel.ts | 53 + .../workbench/common/editor/editorOptions.ts | 44 + .../common/editor/resourceEditorInput.ts | 239 +- .../common/editor/sideBySideEditorInput.ts | 227 ++ .../common/editor/textEditorModel.ts | 2 +- .../common/editor/textResourceEditorInput.ts | 329 +- ...torModel.ts => textResourceEditorModel.ts} | 5 +- src/vs/workbench/common/notifications.ts | 6 +- src/vs/workbench/common/theme.ts | 29 +- .../bulkEdit/browser/bulkEditService.ts | 12 +- .../contrib/bulkEdit/browser/bulkTextEdits.ts | 16 +- .../bulkEdit/browser/preview/bulkEditPane.ts | 6 +- .../browser/callHierarchyPeek.ts | 1 + .../contrib/cli/node/cli.contribution.ts | 193 -- .../browser/codeEditor.contribution.ts | 1 + .../codeEditor/browser/diffEditorHelper.ts | 2 +- .../browser/find/simpleFindWidget.ts | 20 +- .../inspectEditorTokens.ts | 13 +- .../codeEditor/browser/inspectKeybindings.ts | 40 +- .../browser/outline/documentSymbolsOutline.ts | 1 + .../quickaccess/gotoLineQuickAccess.ts | 7 +- .../quickaccess/gotoSymbolQuickAccess.ts | 7 +- .../codeEditor/browser/saveParticipants.ts | 24 +- .../browser/toggleColumnSelection.ts | 79 +- .../codeEditor/browser/toggleMinimap.ts | 63 +- .../browser/toggleMultiCursorModifier.ts | 45 +- .../browser/toggleRenderControlCharacter.ts | 62 +- .../browser/toggleRenderWhitespace.ts | 62 +- .../browser/untitledTextEditorHint.ts} | 33 +- .../comments/browser/commentGlyphWidget.ts | 1 + .../comments/browser/commentThreadWidget.ts | 6 +- .../browser/commentsEditorContribution.ts | 3 +- .../comments/common/commentContextKeys.ts | 2 +- .../comments/common/commentThreadWidget.ts | 2 +- .../browser/customEditor.contribution.ts | 9 +- .../customEditor/browser/customEditorInput.ts | 240 +- .../browser/customEditorInputFactory.ts | 97 +- .../customEditor/browser/customEditors.ts | 10 +- .../customEditor/common/customEditor.ts | 6 +- .../common/customEditorModelManager.ts | 10 + .../common/customTextEditorModel.ts | 16 +- .../browser/breakpointEditorContribution.ts | 3 + .../contrib/debug/browser/breakpointWidget.ts | 8 +- .../contrib/debug/browser/breakpointsView.ts | 32 + .../browser/callStackEditorContribution.ts | 5 + .../contrib/debug/browser/callStackView.ts | 3 +- .../debug/browser/debug.contribution.ts | 17 +- .../debug/browser/debugAdapterManager.ts | 56 +- .../contrib/debug/browser/debugColors.ts | 16 +- .../contrib/debug/browser/debugCommands.ts | 6 +- .../debug/browser/debugEditorActions.ts | 2 +- .../debug/browser/debugEditorContribution.ts | 47 +- .../contrib/debug/browser/debugHover.ts | 1 + .../contrib/debug/browser/debugQuickAccess.ts | 2 +- .../contrib/debug/browser/debugService.ts | 9 +- .../contrib/debug/browser/debugStatus.ts | 3 +- .../debug/browser/media/continue-tb.png | Bin 339 -> 668 bytes .../media/continue-without-debugging-tb.png | Bin 363 -> 1418 bytes .../contrib/debug/browser/media/pause-tb.png | Bin 217 -> 299 bytes .../debug/browser/media/restart-tb.png | Bin 701 -> 1276 bytes .../debug/browser/media/stepinto-tb.png | Bin 418 -> 700 bytes .../debug/browser/media/stepout-tb.png | Bin 411 -> 701 bytes .../debug/browser/media/stepover-tb.png | Bin 548 -> 914 bytes .../contrib/debug/browser/media/stop-tb.png | Bin 197 -> 310 bytes .../contrib/debug/browser/rawDebugSession.ts | 2 +- .../workbench/contrib/debug/browser/repl.ts | 21 +- .../contrib/debug/browser/replViewer.ts | 2 +- .../contrib/debug/browser/welcomeView.ts | 6 +- .../workbench/contrib/debug/common/debug.ts | 1 + .../contrib/debug/common/debugUtils.ts | 2 +- .../contrib/debug/common/debugger.ts | 89 +- .../contrib/debug/node/debugAdapter.ts | 4 +- .../workbench/contrib/debug/node/terminals.ts | 54 +- .../debug/test/browser/callStack.test.ts | 2 +- .../test/browser/debugANSIHandling.test.ts | 2 +- .../contrib/debug/test/node/debugger.test.ts | 14 - .../emmet/test/browser/emmetAction.test.ts | 18 - .../experimentService.test.ts | 2 +- .../abstractRuntimeExtensionsEditor.ts | 39 +- .../extensions/browser/extensionEditor.ts | 11 +- ...onEnablementByWorkspaceTrustRequirement.ts | 36 - ...mentWorkspaceTrustTransitionParticipant.ts | 53 + .../extensionRecommendationsService.ts | 21 +- .../browser/extensions.contribution.ts | 69 +- .../extensions/browser/extensionsActions.ts | 187 +- .../extensionsCompletionItemsProvider.ts | 66 + .../browser/extensionsQuickAccess.ts | 4 +- .../extensions/browser/extensionsViewlet.ts | 66 +- .../extensions/browser/extensionsViews.ts | 133 +- .../browser/extensionsWorkbenchService.ts | 22 +- .../browser/languageRecommendations.ts | 34 + .../extensions/common/extensionQuery.ts | 2 +- .../contrib/extensions/common/extensions.ts | 6 +- .../extensions/common/extensionsInput.ts | 11 +- .../common/runtimeExtensionsInput.ts | 11 +- .../extensionProfileService.ts | 7 +- .../extensions.contribution.ts | 124 +- .../extensionsAutoProfiler.ts | 4 +- .../debugExtensionHostAction.ts | 2 +- .../extensions.contribution.ts | 127 + .../electron-sandbox/extensionsActions.ts | 36 +- .../extensionsSlowActions.ts | 4 +- .../reportExtensionIssueAction.ts | 0 .../runtimeExtensionsEditor.ts | 14 +- .../extensionRecommendationsService.test.ts | 30 +- .../extensionsWorkbenchService.test.ts | 12 +- .../browser/externalTerminal.contribution.ts | 9 +- .../externalTerminal.contribution.ts | 105 +- .../contrib/feedback/browser/feedback.ts | 4 +- .../feedback/browser/feedbackStatusbarItem.ts | 6 +- .../files/browser/editors/binaryFileEditor.ts | 15 +- .../browser/editors/fileEditorHandler.ts | 92 + .../editors/fileEditorInput.ts | 165 +- .../files/browser/editors/textFileEditor.ts | 74 +- .../browser/editors/textFileEditorTracker.ts | 17 +- .../editors/textFileSaveErrorHandler.ts | 2 +- .../contrib/files/browser/explorerService.ts | 36 +- .../contrib/files/browser/explorerViewlet.ts | 4 +- .../files/browser/fileActions.contribution.ts | 33 +- .../contrib/files/browser/fileActions.ts | 345 +-- .../contrib/files/browser/fileCommands.ts | 17 +- .../contrib/files/browser/fileImportExport.ts | 825 +++++ .../files/browser/files.contribution.ts | 56 +- .../workbench/contrib/files/browser/files.ts | 63 +- .../files/browser/files.web.contribution.ts | 10 +- .../files/browser/views/explorerView.ts | 19 +- .../files/browser/views/explorerViewer.ts | 503 +-- .../files/browser/views/openEditorsView.ts | 25 +- .../contrib/files/common/explorerModel.ts | 7 +- .../workbench/contrib/files/common/files.ts | 17 +- .../electron-sandbox/files.contribution.ts | 6 +- .../files/electron-sandbox/textFileEditor.ts | 6 +- .../files/test/browser/explorerModel.test.ts | 10 +- .../files/test/browser/explorerView.test.ts | 2 +- .../test/browser/fileEditorInput.test.ts | 112 +- .../browser/textFileEditorTracker.test.ts | 34 +- .../issue/browser/issue.web.contribution.ts | 49 +- .../contrib/issue/common/commands.ts | 2 + .../electron-sandbox/issue.contribution.ts | 88 +- .../issue/electron-sandbox/issueActions.ts | 52 +- .../browser/localizationsActions.ts | 2 +- .../browser/minimalTranslations.ts | 2 +- .../common/markdownDocumentRenderer.ts | 11 +- .../markers/browser/markers.contribution.ts | 3 +- .../markers/browser/markersTreeViewer.ts | 6 +- .../contrib/notebook/browser/constants.ts | 46 - .../contrib/cellOperations/cellOperations.ts | 14 +- .../test/cellOperations.test.ts | 39 +- .../contrib/clipboard/notebookClipboard.ts | 2 +- .../clipboard/test/notebookClipboard.test.ts | 66 +- .../notebook/browser/contrib/coreActions.ts | 910 ++++-- .../browser/contrib/find/findController.ts | 213 +- .../browser/contrib/find/findModel.ts | 340 +++ .../browser/contrib/find/test/find.test.ts | 195 ++ .../notebook/browser/contrib/fold/folding.ts | 4 +- .../contrib/fold/test/notebookFolding.test.ts | 204 +- .../gettingStarted/notebookGettingStarted.ts | 98 + .../browser/contrib/layout/layoutActions.ts | 8 +- .../contrib/layout/test/layoutActions.test.ts | 2 +- .../browser/contrib/navigation/arrow.ts | 6 +- .../contrib/outline/notebookOutline.ts | 2 +- .../outline/test/notebookOutline.test.ts | 14 +- .../contrib/profile/notebookProfile.ts | 130 + .../browser/contrib/status/editorStatus.ts | 172 +- .../contrib/statusBar/cellStatusBar.ts | 142 - .../contributedStatusBarItemController.ts | 24 +- .../executionStatusBarItemController.ts | 125 +- .../statusBar/notebookVisibleCellObserver.ts | 2 +- .../contrib/statusBar/statusBarProviders.ts | 65 +- .../browser/contrib/troubleshoot/layout.ts | 2 +- .../contrib/undoRedo/notebookUndoRedo.ts | 12 +- .../undoRedo/test/notebookUndoRedo.test.ts | 18 +- .../viewportCustomMarkdown.ts | 24 +- .../notebook/browser/diff/diffComponents.ts | 43 +- .../browser/diff/diffElementOutputs.ts | 9 +- .../browser/diff/notebookDiffActions.ts | 22 +- .../browser/diff/notebookDiffEditorBrowser.ts | 2 + .../browser/diff/notebookTextDiffEditor.ts | 75 +- .../notebook/browser/extensionPoint.ts | 124 +- .../notebook/browser/media/notebook.css | 206 +- .../media/notebookKernelActionViewItem.css | 24 + .../notebook/browser/notebook.contribution.ts | 373 ++- .../notebook/browser/notebookBrowser.ts | 90 +- .../notebookCellStatusBarServiceImpl.ts | 3 +- .../browser/notebookDiffEditorInput.ts | 54 +- .../notebook/browser/notebookEditor.ts | 62 +- .../browser/notebookEditorDecorations.ts | 2 +- .../browser/notebookEditorKernelManager.ts | 26 +- .../browser/notebookEditorServiceImpl.ts | 1 + .../notebook/browser/notebookEditorToolbar.ts | 230 ++ .../notebook/browser/notebookEditorWidget.ts | 823 ++--- .../notebookEditorWidgetContextKeys.ts | 66 +- .../contrib/notebook/browser/notebookIcons.ts | 2 + .../browser/notebookKernelActionViewItem.ts | 102 + .../browser/notebookKernelServiceImpl.ts | 6 +- .../notebookRendererMessagingServiceImpl.ts | 70 + .../notebook/browser/notebookServiceImpl.ts | 289 +- .../notebook/browser/view/notebookCellList.ts | 18 +- .../browser/view/output/outputRenderer.ts | 78 +- .../output/rendererRegistry.ts} | 12 +- .../view/output/transforms/richTransform.ts | 364 +-- .../view/output/transforms/textHelper.ts | 22 +- .../view/renderers/backLayerWebView.ts | 927 ++---- .../browser/view/renderers/cellActionView.ts | 101 +- .../browser/view/renderers/cellContextKeys.ts | 45 +- .../browser/view/renderers/cellDnd.ts | 63 +- .../view/renderers/cellEditorOptions.ts | 144 +- .../browser/view/renderers/cellMenus.ts | 4 + .../browser/view/renderers/cellOutput.ts | 173 +- .../browser/view/renderers/cellRenderer.ts | 234 +- .../browser/view/renderers/cellWidgets.ts | 2 +- .../browser/view/renderers/codeCell.ts | 30 +- .../browser/view/renderers/markdownCell.ts | 54 +- .../browser/view/renderers/webviewMessages.ts | 366 +++ .../browser/view/renderers/webviewPreloads.ts | 1137 ++++--- .../view/renderers/webviewThemeMapping.ts | 19 +- .../browser/viewModel/baseCellViewModel.ts | 46 +- .../browser/viewModel/cellOutputViewModel.ts | 26 +- .../viewModel/cellSelectionCollection.ts | 2 +- .../browser/viewModel/codeCellViewModel.ts | 125 +- .../viewModel/markdownCellViewModel.ts | 102 +- .../browser/viewModel/notebookViewModel.ts | 110 +- .../notebook/browser/viewModel/viewContext.ts | 15 + .../model/notebookCellOutputTextModel.ts | 15 +- .../common/model/notebookCellTextModel.ts | 72 +- .../common/model/notebookTextModel.ts | 76 +- .../contrib/notebook/common/notebookCommon.ts | 238 +- .../notebook/common/notebookEditorInput.ts | 74 +- .../notebook/common/notebookEditorModel.ts | 115 +- .../notebookEditorModelResolverServiceImpl.ts | 44 +- .../notebook/common/notebookKernelService.ts | 3 +- .../common/notebookMarkdownRenderer.ts | 39 - .../notebook/common/notebookOptions.ts | 488 +++ .../notebook/common/notebookOutputRenderer.ts | 32 +- .../notebook/common/notebookPerformance.ts | 2 +- .../notebook/common/notebookProvider.ts | 53 +- .../contrib/notebook/common/notebookRange.ts | 10 +- .../notebookRendererMessagingService.ts | 44 + .../notebook/common/notebookSelector.ts | 31 - .../notebook/common/notebookService.ts | 21 +- .../common/services/notebookSimpleWorker.ts | 12 +- .../services/notebookWorkerServiceImpl.ts | 6 +- .../notebook/test/notebookBrowser.test.ts | 14 +- .../notebook/test/notebookCellList.test.ts | 65 +- .../notebook/test/notebookCommon.test.ts | 12 +- .../notebook/test/notebookDiff.test.ts | 199 +- .../notebook/test/notebookEditor.test.ts | 6 +- .../test/notebookEditorKernelManager.test.ts | 4 +- .../notebook/test/notebookEditorModel.test.ts | 162 +- .../test/notebookKernelService.test.ts | 8 +- .../notebookRendererMessagingService.test.ts | 59 + .../notebook/test/notebookSelection.test.ts | 55 +- .../notebook/test/notebookServiceImpl.test.ts | 10 +- .../notebook/test/notebookTextModel.test.ts | 53 +- .../notebook/test/notebookViewModel.test.ts | 26 +- .../notebook/test/testNotebookEditor.ts | 37 +- .../contrib/outline/browser/outlinePane.ts | 32 +- .../contrib/output/browser/logViewer.ts | 14 +- .../contrib/output/browser/outputView.ts | 19 +- .../output/common/outputChannelModel.ts | 1 - .../performance/browser/perfviewEditor.ts | 16 +- .../electron-sandbox/startupTimings.ts | 17 +- .../preferences/browser/keybindingsEditor.ts | 11 +- .../browser/keybindingsEditorContribution.ts | 1 + .../browser/keyboardLayoutPicker.ts | 7 +- .../browser/preferences.contribution.ts | 20 +- .../preferences/browser/preferencesEditor.ts | 35 +- .../browser/preferencesRenderers.ts | 3 +- .../preferences/browser/preferencesWidgets.ts | 1 + .../preferences/browser/settingsEditor2.ts | 60 +- .../preferences/browser/settingsTree.ts | 9 +- .../preferences/browser/settingsTreeModels.ts | 18 +- .../common/preferencesContribution.ts | 98 +- .../browser/commandsQuickAccess.ts | 6 +- .../quickaccess/browser/viewQuickAccess.ts | 8 +- .../remote/browser/media/tunnelView.css | 10 +- .../contrib/remote/browser/remote.ts | 2 +- .../contrib/remote/browser/remoteExplorer.ts | 45 +- .../contrib/remote/browser/remoteIndicator.ts | 175 +- .../contrib/remote/browser/tunnelView.ts | 219 +- .../remote/common/remote.contribution.ts | 26 + .../contrib/remote/common/tunnelFactory.ts | 13 +- .../contrib/sash/browser/sash.contribution.ts | 4 +- .../workbench/contrib/scm/browser/activity.ts | 3 +- .../contrib/scm/browser/dirtydiffDecorator.ts | 1 + .../contrib/scm/browser/scmViewPane.ts | 9 +- .../search/browser/anythingQuickAccess.ts | 16 +- .../contrib/search/browser/replaceService.ts | 4 +- .../search/browser/search.contribution.ts | 7 +- .../search/browser/searchResultsView.ts | 7 +- .../contrib/search/browser/searchView.ts | 37 +- .../contrib/search/browser/searchWidget.ts | 2 +- .../search/browser/symbolsQuickAccess.ts | 2 +- .../contrib/search/common/queryBuilder.ts | 46 +- .../contrib/search/common/searchModel.ts | 3 + .../search/test/browser/queryBuilder.test.ts | 65 +- .../search/test/common/searchModel.test.ts | 2 +- .../browser/searchEditor.contribution.ts | 77 +- .../searchEditor/browser/searchEditor.ts | 164 +- .../browser/searchEditorActions.ts | 13 +- .../searchEditor/browser/searchEditorInput.ts | 213 +- .../searchEditor/browser/searchEditorModel.ts | 233 +- .../browser/searchEditorSerialization.ts | 3 + .../partsSplash.contribution.ts | 143 - .../surveys/browser/ces.contribution.ts | 17 +- .../browser/languageSurveys.contribution.ts | 32 +- .../electron-sandbox/workspaceTagsService.ts | 5 - .../tasks/browser/abstractTaskService.ts | 48 +- .../tasks/browser/runAutomaticTasks.ts | 22 +- .../tasks/browser/task.contribution.ts | 18 +- .../tasks/browser/taskTerminalStatus.ts | 99 + .../contrib/tasks/browser/tasksQuickAccess.ts | 22 +- .../tasks/browser/terminalTaskSystem.ts | 58 +- .../contrib/tasks/common/jsonSchema_v2.ts | 4 + .../contrib/tasks/common/problemCollectors.ts | 4 +- .../contrib/tasks/common/taskConfiguration.ts | 13 +- .../contrib/tasks/common/taskService.ts | 2 + .../contrib/tasks/common/taskSystem.ts | 1 - .../workbench/contrib/tasks/common/tasks.ts | 5 + .../tasks/electron-sandbox/taskService.ts | 2 +- .../tasks/test/common/configuration.test.ts | 7 +- .../workbench/contrib/terminal/.eslintrc.json | 19 + .../browser/addons/commandTrackerAddon.ts | 20 +- .../browser/environmentVariableInfo.ts | 4 +- .../terminal/browser/links/terminalLink.ts | 8 +- .../browser/links/terminalLinkHelpers.ts | 15 +- .../browser/links/terminalLinkManager.ts | 22 +- .../links/terminalProtocolLinkProvider.ts | 2 +- .../browser/links/terminalWordLinkProvider.ts | 123 +- .../terminal/browser/media/terminal.css | 109 +- .../contrib/terminal/browser/remotePty.ts | 42 +- .../terminal/browser/remoteTerminalService.ts | 90 +- .../terminal/browser/terminal.contribution.ts | 28 +- .../contrib/terminal/browser/terminal.ts | 158 +- .../browser/terminal.web.contribution.ts | 12 +- .../terminal/browser/terminalActions.ts | 543 ++-- .../terminal/browser/terminalConfigHelper.ts | 65 +- .../browser/terminalDecorationsProvider.ts | 2 +- .../terminal/browser/terminalFindWidget.ts | 16 +- .../{terminalTab.ts => terminalGroup.ts} | 198 +- .../contrib/terminal/browser/terminalIcon.ts | 57 + .../terminal/browser/terminalInstance.ts | 693 +++-- .../browser/terminalInstanceService.ts | 93 +- .../contrib/terminal/browser/terminalMenus.ts | 350 +++ .../browser/terminalProcessExtHostProxy.ts | 72 +- .../browser/terminalProcessManager.ts | 172 +- .../browser/terminalProfileResolverService.ts | 286 +- .../terminal/browser/terminalQuickAccess.ts | 46 +- .../terminal/browser/terminalService.ts | 885 +++--- .../terminal/browser/terminalStatusList.ts | 2 +- .../terminal/browser/terminalTabbedView.ts | 277 +- .../terminal/browser/terminalTabsList.ts | 639 ++++ .../terminal/browser/terminalTabsWidget.ts | 359 --- .../browser/terminalTypeAheadAddon.ts | 690 ++--- .../contrib/terminal/browser/terminalView.ts | 306 +- .../widgets/environmentVariableInfoWidget.ts | 2 +- .../browser/widgets/terminalHoverWidget.ts | 9 +- .../terminal/common/remoteTerminalChannel.ts | 92 +- .../contrib/terminal/common/terminal.ts | 403 ++- .../terminal/common/terminalColorRegistry.ts | 7 +- .../terminal/common/terminalConfiguration.ts | 452 +-- .../terminal/common/terminalEnvironment.ts | 32 +- .../common/terminalExtensionPoints.ts | 23 +- .../contrib/terminal/common/terminalMenu.ts | 54 - .../terminal/common/terminalStrings.ts | 2 +- .../electron-browser/terminal.contribution.ts | 34 - .../terminalInstanceService.ts | 60 - .../terminalNativeContribution.ts | 54 - .../terminal/electron-sandbox/localPty.ts | 24 +- .../electron-sandbox/localTerminalService.ts | 66 +- .../electron-sandbox/terminal.contribution.ts | 10 +- .../terminalNativeContribution.ts | 4 +- .../terminalProfileResolverService.ts | 18 +- .../electron-sandbox/terminalRemote.ts | 8 +- .../contrib/terminal/node/terminal.ts | 75 - .../terminal/node/terminalEnvironment.ts | 79 - .../contrib/terminal/node/terminalProfiles.ts | 318 -- .../browser/links/terminalLinkHelpers.test.ts | 6 +- .../links/terminalWordLinkProvider.test.ts | 32 +- .../test/browser/terminalConfigHelper.test.ts | 52 +- .../browser/terminalProcessManager.test.ts | 2 +- .../test/browser/terminalStatusList.test.ts | 2 +- .../test/browser/terminalTypeahead.test.ts | 32 +- .../test/common/terminalColorRegistry.test.ts | 16 +- .../test/common/terminalDataBuffering.test.ts | 2 +- .../test/node/terminalProfiles.test.ts | 100 +- .../browser/explorerProjections/display.ts} | 2 +- .../hierarchalByLocation.ts | 80 +- .../explorerProjections/hierarchalByName.ts | 3 +- .../browser/explorerProjections/index.ts | 33 +- .../explorerProjections/locationStore.ts | 76 - .../contrib/testing/browser/icons.ts | 23 +- .../contrib/testing/browser/media/testing.css | 43 +- .../testing/browser/testExplorerActions.ts | 459 ++- .../testing/browser/testing.contribution.ts | 61 +- .../testing/browser/testingDecorations.ts | 361 ++- .../testing/browser/testingExplorerView.ts | 277 +- .../testing/browser/testingOutputPeek.ts | 1164 ++++++- .../browser/testingOutputTerminalService.ts | 16 +- .../browser/testingProgressUiService.ts | 2 +- .../contrib/testing/common/configuration.ts | 24 +- .../contrib/testing/common/constants.ts | 7 +- .../testing/common/getComputedState.ts | 94 +- .../contrib/testing/common/observableValue.ts | 2 +- .../contrib/testing/common/testCollection.ts | 6 +- .../contrib/testing/common/testResult.ts | 51 +- .../testing/common/testResultService.ts | 5 +- .../testing/common/testResultStorage.ts | 2 +- .../contrib/testing/common/testService.ts | 11 + .../contrib/testing/common/testServiceImpl.ts | 1 - .../contrib/testing/common/testingAutoRun.ts | 63 +- .../testing/common/testingContextKeys.ts | 13 + .../testing/common/testingPeekOpener.ts | 32 + .../common/workspaceTestCollectionService.ts | 155 +- .../testing/test/browser/testObjectTree.ts | 9 +- .../test/common/testResultService.test.ts | 25 +- .../test/common/testResultStorage.test.ts | 4 +- .../contrib/timeline/browser/timelinePane.ts | 7 +- .../update/browser/releaseNotesEditor.ts | 2 +- .../contrib/update/browser/update.ts | 47 +- .../workbench/contrib/update/common/update.ts | 2 +- .../contrib/url/browser/trustedDomains.ts | 16 +- .../url/browser/trustedDomainsValidator.ts | 6 + .../userDataSync/browser/userDataSync.ts | 4 +- .../browser/userDataSyncMergesView.ts | 8 +- .../browser/userDataSyncTrigger.ts | 3 +- .../userDataSync/browser/userDataSyncViews.ts | 116 +- .../webview/browser/baseWebviewElement.ts | 130 +- .../browser/dynamicWebviewEditorOverlay.ts | 66 +- .../contrib/webview/browser/pre/host.js | 9 +- .../contrib/webview/browser/pre/main.js | 184 +- .../webview/browser/pre/service-worker.js | 131 +- .../webview/browser/resourceLoading.ts | 72 +- .../contrib/webview/browser/themeing.ts | 57 +- .../webview/browser/webview.contribution.ts | 41 + .../contrib/webview/browser/webview.ts | 60 +- .../contrib/webview/browser/webviewElement.ts | 31 +- .../webview/browser/webviewFindWidget.ts | 10 +- .../contrib/webview/browser/webviewService.ts | 19 +- .../contrib/webview/common/webviewUri.ts | 25 - .../electron-browser/webviewElement.ts | 15 +- .../electron-browser/webviewService.ts | 6 +- .../electron-sandbox/iframeWebviewElement.ts | 26 +- .../webviewPanel/browser/webviewCommands.ts | 10 +- .../webviewPanel/browser/webviewEditor.ts | 6 +- .../browser/webviewEditorInput.ts | 11 +- .../browser/webviewPanel.contribution.ts | 34 +- .../webviewView/browser/webviewViewPane.ts | 159 +- .../browser/gettingStarted.contribution.ts | 89 +- .../gettingStarted/browser/gettingStarted.css | 67 +- .../gettingStarted/browser/gettingStarted.ts | 474 ++- .../browser/gettingStartedExtensionPoint.ts | 72 +- .../browser/gettingStartedInput.ts | 7 +- .../browser/gettingStartedService.ts | 438 ++- .../common/gettingStartedContent.ts | 238 +- .../gettingStarted/common/media/dark.png | Bin 0 -> 14165 bytes .../common/media/dark/keymaps.png | Bin 49149 -> 0 bytes .../common/media/dark/openVSC.png | Bin 7282 -> 0 bytes .../common/media/dark/workspaceTrust.svg | 40 + .../common/media/example_markdown_media.ts | 29 + .../common/media/forwardPorts.png | Bin 17327 -> 0 bytes .../common/media/hc/keymaps.png | Bin 50010 -> 0 bytes .../gettingStarted/common/media/light.png | Bin 0 -> 14341 bytes .../common/media/light/keymaps.png | Bin 49954 -> 0 bytes .../common/media/light/openVSC.png | Bin 17144 -> 0 bytes .../common/media/light/workspaceTrust.svg | 40 + .../gettingStarted/common/media/monokai.png | Bin 0 -> 14287 bytes .../gettingStarted/common/media/more.png | Bin 0 -> 17121 bytes .../common/media/notebookProfile.ts | 29 + .../common/media/notebookThemes/colab.png | Bin 0 -> 2473 bytes .../common/media/notebookThemes/default.png | Bin 0 -> 2476 bytes .../common/media/notebookThemes/jupyter.png | Bin 0 -> 2489 bytes .../common/media/pullRequests.png | Bin 51090 -> 0 bytes .../common/media/quiet-light.png | Bin 0 -> 14270 bytes .../common/media/remoteTerminal.png | Bin 49686 -> 0 bytes .../common/media/runProject.png | Bin 65764 -> 0 bytes .../page/browser/welcomePage.contribution.ts | 30 +- .../welcome/page/browser/welcomePage.ts | 110 +- .../browser/telemetryOptOut.ts | 1 + .../browser/editor/editorWalkThrough.ts | 7 +- .../walkThrough/browser/walkThroughInput.ts | 8 +- .../walkThrough/browser/walkThroughPart.ts | 9 +- .../browser/workspace.contribution.ts | 564 +++- .../workspace/browser/workspaceTrustColors.ts | 14 - .../browser/workspaceTrustEditor.css | 147 +- .../workspace/browser/workspaceTrustEditor.ts | 861 ++++-- .../workspace/browser/workspaceTrustTree.ts | 624 ---- .../browser/workspaces.contribution.ts | 14 +- .../actions/installActions.ts | 74 + .../electron-sandbox/actions/windowActions.ts | 249 +- .../electron-sandbox/desktop.contribution.ts | 182 +- .../parts/dialogs/dialogHandler.ts | 4 +- .../parts/titlebar/menubarControl.ts | 8 +- .../sandbox.simpleservices.ts | 53 +- .../electron-sandbox/shared.desktop.main.ts | 6 +- src/vs/workbench/electron-sandbox/splash.ts | 112 + src/vs/workbench/electron-sandbox/window.ts | 82 +- .../services/activity/common/activity.ts | 8 +- .../browser/authenticationService.ts | 24 +- .../services/banner/browser/bannerService.ts | 31 + .../clipboard/browser/clipboardService.ts | 11 +- .../configuration/common/configuration.ts | 11 +- .../configuration/test/common/testServices.ts | 2 +- .../browser/configurationResolverService.ts | 11 +- .../common/configurationResolverUtils.ts | 2 +- .../common/variableResolver.ts | 6 +- .../configurationResolverService.ts | 7 +- .../configurationResolverService.test.ts | 39 +- .../electron-sandbox/contextmenuService.ts | 7 + .../credentials/browser/credentialsService.ts | 8 +- .../decorations/browser/decorationsService.ts | 22 +- .../dialogs/browser/simpleFileDialog.ts | 8 +- .../services/dialogs/common/dialogService.ts | 12 +- .../editor/browser/codeEditorService.ts | 6 +- .../editor/browser/editorOverrideService.ts | 336 +- .../services/editor/browser/editorService.ts | 441 ++- .../editor/common/editorGroupsService.ts | 24 +- .../editor/common/editorOverrideService.ts | 137 +- .../services/editor/common/editorService.ts | 66 +- .../test/browser/editorGroupsService.test.ts | 83 +- .../editor/test/browser/editorService.test.ts | 269 +- .../test/browser/editorsObserver.test.ts | 84 +- .../environment/browser/environmentService.ts | 25 +- .../environment/common/environmentService.ts | 2 - .../electron-sandbox/environmentService.ts | 15 - .../shellEnvironmentService.ts | 2 +- .../browser/extensionBisect.ts | 3 +- .../browser/extensionEnablementService.ts | 41 +- .../common/extensionManagement.ts | 20 +- .../common/extensionManagementService.ts | 19 +- .../common/webExtensionsScannerService.ts | 32 +- .../extensionManagementService.ts | 6 +- .../remoteExtensionManagementService.ts | 6 +- .../extensionEnablementService.test.ts | 5 +- .../common/extensionRecommendations.ts | 1 + .../extensions/browser/extensionService.ts | 4 +- .../extensions/browser/extensionUrlHandler.ts | 12 +- .../browser/webWorkerExtensionHost.ts | 2 - .../common/abstractExtensionService.ts | 157 +- .../extensions/common/extensionHostMain.ts | 16 +- .../extensions/common/extensionHostManager.ts | 14 + .../extensionManifestPropertiesService.ts | 25 +- .../extensions/common/extensionsRegistry.ts | 34 +- .../extensions/common/remoteExtensionHost.ts | 4 +- .../cachedExtensionScanner.ts | 27 +- .../electron-browser/extensionService.ts | 79 +- .../localProcessExtensionHost.ts | 8 +- .../node/extensionHostProcessSetup.ts | 7 +- .../extensions/node/extensionPoints.ts | 43 +- ...extensionManifestPropertiesService.test.ts | 27 +- .../files/browser/elevatedFileService.ts | 2 +- .../files/common/elevatedFileService.ts | 2 +- .../electron-sandbox/elevatedFileService.ts | 2 +- .../services/history/browser/history.ts | 7 +- .../history/test/browser/history.test.ts | 29 +- .../host/browser/browserHostService.ts | 10 +- .../workbench/services/host/browser/host.ts | 2 +- .../services/hover/browser/hoverService.ts | 24 +- .../services/hover/browser/hoverWidget.ts | 3 +- .../issue/electron-sandbox/issueService.ts | 29 +- .../browser/keyboardLayouts/_.contribution.ts | 2 +- .../browser/keyboardLayouts/de.linux.ts | 2 +- .../browser/keyboardLayouts/de.win.ts | 2 +- .../browser/keyboardLayouts/dk.win.ts | 2 +- .../browser/keyboardLayouts/en-ext.darwin.ts | 2 +- .../browser/keyboardLayouts/en-in.win.ts | 2 +- .../browser/keyboardLayouts/en-intl.darwin.ts | 2 +- .../browser/keyboardLayouts/en-uk.darwin.ts | 2 +- .../browser/keyboardLayouts/en-uk.win.ts | 2 +- .../browser/keyboardLayouts/en.darwin.ts | 2 +- .../browser/keyboardLayouts/en.win.ts | 2 +- .../browser/keyboardLayouts/es-latin.win.ts | 2 +- .../browser/keyboardLayouts/es.darwin.ts | 2 +- .../browser/keyboardLayouts/es.linux.ts | 2 +- .../browser/keyboardLayouts/es.win.ts | 2 +- .../browser/keyboardLayouts/fr.win.ts | 2 +- .../browser/keyboardLayouts/it.darwin.ts | 2 +- .../browser/keyboardLayouts/it.win.ts | 2 +- .../keyboardLayouts/jp-roman.darwin.ts | 2 +- .../browser/keyboardLayouts/jp.darwin.ts | 2 +- .../browser/keyboardLayouts/ko.darwin.ts | 2 +- .../layout.contribution.linux.ts | 2 +- .../layout.contribution.win.ts | 2 +- .../browser/keyboardLayouts/pl.darwin.ts | 2 +- .../browser/keyboardLayouts/pl.win.ts | 2 +- .../browser/keyboardLayouts/pt-br.win.ts | 2 +- .../browser/keyboardLayouts/pt.win.ts | 2 +- .../browser/keyboardLayouts/ru.darwin.ts | 2 +- .../browser/keyboardLayouts/ru.win.ts | 2 +- .../browser/keyboardLayouts/sv.darwin.ts | 2 +- .../browser/keyboardLayouts/tr.win.ts | 2 +- .../browser/keyboardLayouts/zh-hans.darwin.ts | 2 +- .../keybinding/browser/navigatorKeyboard.ts | 2 +- .../common/macLinuxKeyboardMapper.ts | 1 + .../keyboardMapperTestUtils.ts | 9 +- .../test/electron-browser/linux_en_us.js | 2 +- .../test/electron-browser/win_en_us.js | 2 +- .../test/electron-browser/win_por_ptb.js | 2 +- .../services/layout/browser/layoutService.ts | 6 + .../lifecycle/common/lifecycleService.ts | 4 +- .../log/electron-sandbox/logService.ts | 2 +- .../services/path/browser/pathService.ts | 31 +- .../services/path/common/pathService.ts | 36 +- .../path/electron-sandbox/pathService.ts | 11 +- .../browser/keybindingsEditorInput.ts | 54 + .../browser/keybindingsEditorModel.ts | 2 +- .../preferences/browser/preferencesService.ts | 57 +- .../preferences/common/preferences.ts | 41 +- .../preferencesEditorInput.ts | 73 +- .../preferences/common/preferencesModels.ts | 2 +- .../test/browser/preferencesService.test.ts | 12 +- .../progress/browser/progressService.ts | 51 +- .../remote/common/remoteExplorerService.ts | 199 +- .../electron-browser/tunnelServiceImpl.ts | 6 +- .../remote/test/common/testServices.ts | 2 +- .../services/search/browser/searchService.ts | 2 +- .../services/search/common/replace.ts | 2 +- .../services/search/common/search.ts | 12 +- .../services/search/common/searchExtTypes.ts | 20 +- .../services/search/common/searchHelpers.ts | 2 +- .../services/search/common/searchService.ts | 2 +- .../search/electron-browser/searchService.ts | 4 + .../services/search/node/fileSearch.ts | 4 +- .../search/node/ripgrepTextSearchEngine.ts | 12 +- .../services/search/node/searchApp.ts | 2 +- .../services/search/node/searchIpc.ts | 2 +- .../services/search/node/textSearchManager.ts | 2 +- .../search/test/common/replace.test.ts | 14 + .../search/test/common/searchHelpers.test.ts | 35 +- .../test/node/ripgrepTextSearchEngine.test.ts | 2 + .../services/statusbar/common/statusbar.ts | 13 +- .../electron-browser/commonProperties.test.ts | 6 +- .../browser/abstractTextMateService.ts | 25 +- .../textMate/browser/textMateService.ts | 22 - .../electron-sandbox/textMateService.ts | 4 +- .../browser/browserTextFileService.ts | 45 +- .../textfile/browser/textFileService.ts | 10 +- .../textfile/common/textFileEditorModel.ts | 29 +- .../common/textFileEditorModelManager.ts | 71 +- .../services/textfile/common/textfiles.ts | 1 + .../electron-sandbox/nativeTextFileService.ts | 5 +- .../test/browser/textFileEditorModel.test.ts | 800 +++++ .../textFileEditorModelManager.test.ts | 324 ++ .../test/browser/textFileService.test.ts | 167 + .../nativeTextFileService.io.test.ts | 13 +- .../nativeTextFileService.test.ts | 2 +- .../test/node/encoding/fixtures/some_ansi.css | 4 +- .../common/textModelResolverService.ts | 25 +- .../browser/textModelResolverService.test.ts | 210 ++ .../browser/browserHostColorSchemeService.ts | 5 +- .../themes/common/iconExtensionPoint.ts | 6 +- .../themes/common/themeConfiguration.ts | 1 + .../services/timer/browser/timerService.ts | 11 - .../timer/electron-sandbox/timerService.ts | 35 +- .../common/untitledTextEditorHandler.ts | 121 + .../common/untitledTextEditorInput.ts | 35 +- .../common/untitledTextEditorModel.ts | 79 +- .../test/browser/untitledTextEditor.test.ts | 29 +- .../services/url/browser/urlService.ts | 32 +- .../services/userData/browser/userDataInit.ts | 2 +- .../userDataSyncResourceEnablementService.ts | 2 +- .../electron-sandbox/userDataSyncService.ts | 2 +- .../test/browser/viewContainerModel.test.ts | 518 ++++ .../common/abstractFileWorkingCopyManager.ts | 169 + .../workingCopy/common/fileWorkingCopy.ts | 1287 +------- .../common/fileWorkingCopyManager.ts | 845 ++--- .../common/legacyBackupRestorer.ts | 138 - .../workingCopy/common/resourceWorkingCopy.ts | 160 + .../common/storedFileWorkingCopy.ts | 1212 ++++++++ .../common/storedFileWorkingCopyManager.ts | 585 ++++ .../common/untitledFileWorkingCopy.ts | 284 ++ .../common/untitledFileWorkingCopyManager.ts | 256 ++ .../workingCopy/common/workingCopy.ts | 2 +- .../workingCopy/common/workingCopyBackup.ts | 8 +- .../common/workingCopyBackupService.ts | 27 +- .../common/workingCopyBackupTracker.ts | 17 +- .../common/workingCopyEditorService.ts | 4 +- .../workingCopy/common/workingCopyService.ts | 29 +- .../workingCopyBackupTracker.ts | 91 +- .../browser/fileWorkingCopyManager.test.ts | 493 +-- .../test/browser/legacyBackupRestorer.test.ts | 113 - .../test/browser/resourceWorkingCopyTest.ts | 84 + ....test.ts => storedFileWorkingCopy.test.ts} | 222 +- .../storedFileWorkingCopyManager.test.ts | 527 ++++ .../browser/untitledFileWorkingCopy.test.ts | 308 ++ .../untitledFileWorkingCopyManager.test.ts | 238 ++ .../browser/workingCopyBackupTracker.test.ts | 16 +- .../browser/workingCopyEditorService.test.ts | 5 +- .../test/common/workingCopyService.test.ts | 7 + .../workingCopyBackupService.test.ts | 30 +- .../workingCopyBackupTracker.test.ts | 55 +- .../abstractWorkspaceEditingService.ts | 12 +- .../browser/workspaceTrustEditorInput.ts | 4 +- .../workspaces/browser/workspacesService.ts | 2 +- .../workspaces/common/workspaceTrust.ts | 666 +++- .../workspaceEditingService.ts | 2 +- .../test/common/testWorkspaceTrustService.ts | 64 +- .../browser/api/extHostApiCommands.test.ts | 65 +- .../test/browser/api/extHostNotebook.test.ts | 51 +- .../api/extHostNotebookConcatDocument.test.ts | 33 +- .../api/extHostNotebookKernel2.test.ts | 154 +- .../test/browser/api/extHostTreeViews.test.ts | 26 +- .../browser/api/extHostTypeConverter.test.ts | 32 +- .../test/browser/api/extHostTypes.test.ts | 80 +- .../test/browser/api/extHostWebview.test.ts | 104 +- .../parts/editor/diffEditorInput.test.ts | 75 + .../test/browser/parts/editor/editor.test.ts | 144 +- .../parts/editor/editorDiffModel.test.ts | 10 +- .../parts/editor/editorGroupModel.test.ts | 12 +- .../browser/parts/editor/editorInput.test.ts | 64 +- .../browser/parts/editor/editorModel.test.ts | 6 +- .../browser/parts/editor/editorPane.test.ts | 189 +- .../parts/editor/resourceEditorInput.test.ts | 61 +- .../editor/sideBySideEditorInput.test.ts | 61 + .../editor/textResourceEditorInput.test.ts | 108 + .../test/browser/quickAccess.test.ts | 331 ++ .../test/browser/workbenchTestServices.ts | 177 +- .../test/common/notifications.test.ts | 26 +- .../test/common/workbenchTestServices.ts | 2 +- .../api/extHostSearch.test.ts | 50 +- .../api/mainThreadWorkspace.test.ts | 4 +- .../colorRegistry.releaseTest.ts | 5 +- .../colorRegistryExport.test.ts | 20 + .../electron-browser/workbenchTestServices.ts | 11 +- src/vs/workbench/workbench.common.main.ts | 5 +- src/vs/workbench/workbench.desktop.main.css | 2 +- .../workbench/workbench.desktop.main.nls.js | 2 +- src/vs/workbench/workbench.desktop.main.ts | 24 - .../workbench.desktop.sandbox.main.ts | 2 + src/vs/workbench/workbench.sandbox.main.ts | 6 + src/vs/workbench/workbench.web.api.ts | 24 + src/vs/workbench/workbench.web.main.ts | 1 + test/automation/package.json | 2 +- test/automation/src/application.ts | 2 +- test/automation/src/code.ts | 5 +- test/automation/src/driver.js | 2 +- test/automation/src/logger.ts | 2 +- test/automation/src/playwrightDriver.ts | 1 + test/automation/src/search.ts | 5 + test/automation/yarn.lock | 8 +- test/integration/browser/package.json | 2 +- test/integration/browser/src/index.ts | 4 +- test/integration/browser/yarn.lock | 8 +- test/leaks/index.html | 40 + test/leaks/package.json | 11 + test/leaks/server.js | 16 + test/leaks/yarn.lock | 371 +++ test/smoke/package.json | 2 +- .../src/areas/multiroot/multiroot.test.ts | 6 +- .../smoke/src/areas/notebook/notebook.test.ts | 2 +- test/smoke/src/areas/search/search.test.ts | 9 + .../src/areas/workbench/localization.test.ts | 5 +- test/smoke/src/main.ts | 4 +- test/smoke/yarn.lock | 8 +- test/unit/browser/index.js | 2 +- test/unit/browser/renderer.html | 5 +- test/unit/electron/index.js | 3 +- test/unit/electron/renderer.js | 2 + test/unit/node/all.js | 1 + test/unit/node/index.html | 3 +- yarn.lock | 316 +- 1752 files changed, 59525 insertions(+), 33878 deletions(-) delete mode 100644 build/azure-pipelines/common/sync-mooncake.js delete mode 100644 build/azure-pipelines/common/sync-mooncake.ts delete mode 100755 build/azure-pipelines/darwin/publish-server.sh delete mode 100755 build/azure-pipelines/linux/alpine/publish.sh rename build/azure-pipelines/linux/{publish.sh => prepare-publish.sh} (79%) create mode 100644 build/azure-pipelines/product-publish.ps1 create mode 100644 build/azure-pipelines/product-publish.yml rename build/azure-pipelines/{release.yml => product-release.yml} (100%) delete mode 100644 build/azure-pipelines/sync-mooncake.yml delete mode 100755 build/azure-pipelines/web/publish.sh rename build/azure-pipelines/win32/{publish.ps1 => prepare-publish.ps1} (51%) create mode 100644 build/lib/eslint/vscode-dts-vscode-in-comments.js create mode 100644 build/lib/eslint/vscode-dts-vscode-in-comments.ts create mode 100644 extensions/dart/.vscodeignore create mode 100644 extensions/dart/cgmanifest.json create mode 100644 extensions/dart/language-configuration.json create mode 100644 extensions/dart/package.json create mode 100644 extensions/dart/package.nls.json create mode 100644 extensions/dart/syntaxes/dart.tmLanguage.json delete mode 100644 extensions/markdown-language-features/media/index.js delete mode 100644 extensions/markdown-language-features/media/pre.js create mode 100644 extensions/markdown-language-features/src/commands/reloadPlugins.ts rename extensions/{notebook-markdown-extensions => markdown-math}/.gitignore (100%) rename extensions/{notebook-markdown-extensions => markdown-math}/.vscodeignore (100%) rename extensions/{notebook-markdown-extensions => markdown-math}/README.md (75%) rename extensions/{notebook-markdown-extensions => markdown-math}/esbuild.js (91%) create mode 100644 extensions/markdown-math/extension-browser.webpack.config.js create mode 100644 extensions/markdown-math/extension.webpack.config.js rename extensions/{notebook-markdown-extensions => markdown-math}/icon.png (100%) create mode 100644 extensions/markdown-math/notebook/katex.ts rename extensions/{notebook-markdown-extensions => markdown-math}/notebook/tsconfig.json (100%) create mode 100644 extensions/markdown-math/package.json create mode 100644 extensions/markdown-math/package.nls.json rename extensions/{notebook-markdown-extensions/notebook/emoji.ts => markdown-math/preview-styles/index.css} (54%) create mode 100644 extensions/markdown-math/src/extension.ts create mode 100644 extensions/markdown-math/src/types.d.ts create mode 100644 extensions/markdown-math/tsconfig.json create mode 100644 extensions/markdown-math/yarn.lock delete mode 100644 extensions/notebook-markdown-extensions/notebook/katex.ts delete mode 100644 extensions/notebook-markdown-extensions/package.json delete mode 100644 extensions/notebook-markdown-extensions/package.nls.json delete mode 100644 extensions/notebook-markdown-extensions/yarn.lock create mode 100644 extensions/vscode-colorize-tests/test/colorize-fixtures/test.dart create mode 100644 extensions/vscode-colorize-tests/test/colorize-results/test_dart.json delete mode 100644 extensions/vscode-notebook-tests/media/icon.png rename src/sql/workbench/contrib/query/{common => browser}/fileQueryEditorInput.ts (96%) delete mode 100644 src/sql/workbench/services/languageAssociation/common/doHandleUpgrade.ts create mode 100644 src/vs/base/browser/ui/iconLabel/iconLabelHover.ts create mode 100644 src/vs/base/common/ports.ts create mode 100644 src/vs/base/test/common/testUtils.ts create mode 100644 src/vs/base/test/parts/quickinput/browser/quickinput.test.ts create mode 100644 src/vs/code/electron-browser/sharedProcess/contrib/codeCacheCleaner.ts delete mode 100644 src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts create mode 100644 src/vs/editor/contrib/hover/colorHoverParticipant.ts create mode 100644 src/vs/editor/contrib/hover/hoverTypes.ts rename src/vs/editor/contrib/{inlineHints/inlineHintsController.ts => inlayHints/inlayHintsController.ts} (68%) create mode 100644 src/vs/editor/contrib/inlineCompletions/ghostText.css create mode 100644 src/vs/editor/contrib/inlineCompletions/ghostTextController.ts create mode 100644 src/vs/editor/contrib/inlineCompletions/ghostTextWidget.ts create mode 100644 src/vs/editor/contrib/inlineCompletions/inlineCompletionsHoverParticipant.ts create mode 100644 src/vs/editor/contrib/inlineCompletions/inlineCompletionsModel.ts create mode 100644 src/vs/editor/contrib/inlineCompletions/suggestWidgetAdapterModel.ts create mode 100644 src/vs/editor/contrib/inlineCompletions/utils.ts rename src/vs/{base/browser/ui/dropdown => platform/actions/browser}/dropdownWithPrimaryActionViewItem.ts (63%) create mode 100644 src/vs/platform/contextkey/test/browser/contextkey.test.ts rename src/vs/{workbench/contrib => platform}/externalTerminal/common/externalTerminal.ts (67%) rename src/vs/{workbench/contrib/externalTerminal/node => platform/externalTerminal/electron-main}/externalTerminalService.test.ts (95%) create mode 100644 src/vs/platform/externalTerminal/electron-sandbox/externalTerminalMainService.ts rename src/vs/{workbench/contrib => platform}/externalTerminal/node/externalTerminalService.ts (75%) rename src/vs/platform/state/{node => electron-main}/state.ts (72%) create mode 100644 src/vs/platform/state/electron-main/stateMainService.ts delete mode 100644 src/vs/platform/state/node/stateService.ts create mode 100644 src/vs/platform/state/test/electron-main/state.test.ts delete mode 100644 src/vs/platform/state/test/node/state.test.ts create mode 100644 src/vs/platform/terminal/common/terminalEnvironment.ts create mode 100644 src/vs/platform/terminal/common/terminalPlatformConfiguration.ts create mode 100644 src/vs/platform/terminal/node/terminalProfiles.ts create mode 100644 src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts create mode 100644 src/vs/workbench/api/browser/mainThreadNotebookRenderers.ts create mode 100644 src/vs/workbench/api/common/extHostNotebookDocuments.ts create mode 100644 src/vs/workbench/api/common/extHostNotebookEditors.ts create mode 100644 src/vs/workbench/api/common/extHostNotebookRenderers.ts rename src/vs/workbench/browser/{menuActions.ts => actions.ts} (99%) create mode 100644 src/vs/workbench/browser/parts/banner/bannerPart.ts create mode 100644 src/vs/workbench/browser/parts/banner/media/bannerpart.css create mode 100644 src/vs/workbench/browser/parts/editor/media/workspacetrusteditor.css delete mode 100644 src/vs/workbench/browser/parts/editor/rangeDecorations.ts create mode 100644 src/vs/workbench/browser/parts/editor/workspaceTrustRequiredEditor.ts create mode 100644 src/vs/workbench/common/editor/editorInput.ts create mode 100644 src/vs/workbench/common/editor/editorModel.ts create mode 100644 src/vs/workbench/common/editor/editorOptions.ts create mode 100644 src/vs/workbench/common/editor/sideBySideEditorInput.ts rename src/vs/workbench/common/editor/{resourceEditorModel.ts => textResourceEditorModel.ts} (85%) delete mode 100644 src/vs/workbench/contrib/cli/node/cli.contribution.ts rename src/vs/workbench/{browser/parts/editor/untitledHint.ts => contrib/codeEditor/browser/untitledTextEditorHint.ts} (83%) delete mode 100644 src/vs/workbench/contrib/extensions/browser/extensionEnablementByWorkspaceTrustRequirement.ts create mode 100644 src/vs/workbench/contrib/extensions/browser/extensionEnablementWorkspaceTrustTransitionParticipant.ts create mode 100644 src/vs/workbench/contrib/extensions/browser/extensionsCompletionItemsProvider.ts create mode 100644 src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts rename src/vs/workbench/contrib/extensions/{electron-browser => electron-sandbox}/debugExtensionHostAction.ts (97%) create mode 100644 src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts rename src/vs/workbench/contrib/extensions/{electron-browser => electron-sandbox}/extensionsSlowActions.ts (99%) rename src/vs/workbench/contrib/extensions/{electron-browser => electron-sandbox}/reportExtensionIssueAction.ts (100%) rename src/vs/workbench/contrib/extensions/{electron-browser => electron-sandbox}/runtimeExtensionsEditor.ts (95%) rename src/vs/workbench/contrib/externalTerminal/{node => electron-sandbox}/externalTerminal.contribution.ts (50%) create mode 100644 src/vs/workbench/contrib/files/browser/editors/fileEditorHandler.ts rename src/vs/workbench/contrib/files/{common => browser}/editors/fileEditorInput.ts (72%) create mode 100644 src/vs/workbench/contrib/files/browser/fileImportExport.ts delete mode 100644 src/vs/workbench/contrib/notebook/browser/constants.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/contrib/find/test/find.test.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/contrib/gettingStarted/notebookGettingStarted.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/contrib/profile/notebookProfile.ts delete mode 100644 src/vs/workbench/contrib/notebook/browser/contrib/statusBar/cellStatusBar.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/media/notebookKernelActionViewItem.css create mode 100644 src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/notebookKernelActionViewItem.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl.ts rename src/vs/workbench/contrib/notebook/browser/{notebookRegistry.ts => view/output/rendererRegistry.ts} (66%) create mode 100644 src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/viewModel/viewContext.ts delete mode 100644 src/vs/workbench/contrib/notebook/common/notebookMarkdownRenderer.ts create mode 100644 src/vs/workbench/contrib/notebook/common/notebookOptions.ts create mode 100644 src/vs/workbench/contrib/notebook/common/notebookRendererMessagingService.ts delete mode 100644 src/vs/workbench/contrib/notebook/common/notebookSelector.ts create mode 100644 src/vs/workbench/contrib/notebook/test/notebookRendererMessagingService.test.ts delete mode 100644 src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts create mode 100644 src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts create mode 100644 src/vs/workbench/contrib/terminal/.eslintrc.json rename src/vs/workbench/contrib/terminal/browser/{terminalTab.ts => terminalGroup.ts} (73%) create mode 100644 src/vs/workbench/contrib/terminal/browser/terminalIcon.ts create mode 100644 src/vs/workbench/contrib/terminal/browser/terminalMenus.ts create mode 100644 src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts delete mode 100644 src/vs/workbench/contrib/terminal/browser/terminalTabsWidget.ts delete mode 100644 src/vs/workbench/contrib/terminal/common/terminalMenu.ts delete mode 100644 src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts delete mode 100644 src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts delete mode 100644 src/vs/workbench/contrib/terminal/electron-browser/terminalNativeContribution.ts delete mode 100644 src/vs/workbench/contrib/terminal/node/terminal.ts delete mode 100644 src/vs/workbench/contrib/terminal/node/terminalEnvironment.ts delete mode 100644 src/vs/workbench/contrib/terminal/node/terminalProfiles.ts rename src/vs/workbench/contrib/{externalTerminal/node/externalTerminal.ts => testing/browser/explorerProjections/display.ts} (87%) delete mode 100644 src/vs/workbench/contrib/testing/browser/explorerProjections/locationStore.ts create mode 100644 src/vs/workbench/contrib/testing/common/testingPeekOpener.ts delete mode 100644 src/vs/workbench/contrib/webview/common/webviewUri.ts create mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark.png delete mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark/keymaps.png delete mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark/openVSC.png create mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark/workspaceTrust.svg create mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/example_markdown_media.ts delete mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/forwardPorts.png delete mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/hc/keymaps.png create mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/light.png delete mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/light/keymaps.png delete mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/light/openVSC.png create mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/light/workspaceTrust.svg create mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/monokai.png create mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/more.png create mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/notebookProfile.ts create mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/notebookThemes/colab.png create mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/notebookThemes/default.png create mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/notebookThemes/jupyter.png delete mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/pullRequests.png create mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/quiet-light.png delete mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/remoteTerminal.png delete mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/runProject.png delete mode 100644 src/vs/workbench/contrib/workspace/browser/workspaceTrustColors.ts delete mode 100644 src/vs/workbench/contrib/workspace/browser/workspaceTrustTree.ts create mode 100644 src/vs/workbench/electron-sandbox/actions/installActions.ts create mode 100644 src/vs/workbench/electron-sandbox/splash.ts create mode 100644 src/vs/workbench/services/banner/browser/bannerService.ts create mode 100644 src/vs/workbench/services/preferences/browser/keybindingsEditorInput.ts rename src/vs/workbench/services/preferences/{browser => common}/preferencesEditorInput.ts (61%) create mode 100644 src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts create mode 100644 src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts create mode 100644 src/vs/workbench/services/textfile/test/browser/textFileService.test.ts create mode 100644 src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts create mode 100644 src/vs/workbench/services/untitled/common/untitledTextEditorHandler.ts create mode 100644 src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts create mode 100644 src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts delete mode 100644 src/vs/workbench/services/workingCopy/common/legacyBackupRestorer.ts create mode 100644 src/vs/workbench/services/workingCopy/common/resourceWorkingCopy.ts create mode 100644 src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts create mode 100644 src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts create mode 100644 src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts create mode 100644 src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts delete mode 100644 src/vs/workbench/services/workingCopy/test/browser/legacyBackupRestorer.test.ts create mode 100644 src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopyTest.ts rename src/vs/workbench/services/workingCopy/test/browser/{fileWorkingCopy.test.ts => storedFileWorkingCopy.test.ts} (68%) create mode 100644 src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts create mode 100644 src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts create mode 100644 src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts create mode 100644 src/vs/workbench/test/browser/parts/editor/diffEditorInput.test.ts create mode 100644 src/vs/workbench/test/browser/parts/editor/sideBySideEditorInput.test.ts create mode 100644 src/vs/workbench/test/browser/parts/editor/textResourceEditorInput.test.ts create mode 100644 src/vs/workbench/test/browser/quickAccess.test.ts create mode 100644 src/vs/workbench/test/electron-browser/colorRegistryExport.test.ts create mode 100644 test/leaks/index.html create mode 100644 test/leaks/package.json create mode 100644 test/leaks/server.js create mode 100644 test/leaks/yarn.lock diff --git a/.devcontainer/README.md b/.devcontainer/README.md index e5597559f4..827166823d 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -1,14 +1,14 @@ # Code - OSS Development Container -This repository includes configuration for a development container for working with Code - OSS in an isolated local container or using [GitHub Codespaces](https://github.com/features/codespaces). +This repository includes configuration for a development container for working with Code - OSS in a local container or using [GitHub Codespaces](https://github.com/features/codespaces). -> **Tip:** The default VNC password is `vscode`. The VNC server runs on port `5901` with a web client at `6080`. For better performance, we recommend using a [VNC Viewer](https://www.realvnc.com/en/connect/download/viewer/). Applications like the macOS Screen Sharing app will not perform as well. +> **Tip:** The default VNC password is `vscode`. The VNC server runs on port `5901` and a web client is available on port `6080`. ## Quick start - local 1. Install Docker Desktop or Docker for Linux on your local machine. (See [docs](https://aka.ms/vscode-remote/containers/getting-started) for additional details.) -2. **Important**: Docker needs at least **4 Cores and 6 GB of RAM (8 GB recommended)** to run full build. If you on macOS, or using the old Hyper-V engine for Windows, update these values for Docker Desktop by right-clicking on the Docker status bar item, going to **Preferences/Settings > Resources > Advanced**. +2. **Important**: Docker needs at least **4 Cores and 6 GB of RAM (8 GB recommended)** to run a full build. If you are on macOS, or are using the old Hyper-V engine for Windows, update these values for Docker Desktop by right-clicking on the Docker status bar item and going to **Preferences/Settings > Resources > Advanced**. > **Note:** The [Resource Monitor](https://marketplace.visualstudio.com/items?itemName=mutantdino.resourcemonitor) extension is included in the container so you can keep an eye on CPU/Memory in the status bar. @@ -16,53 +16,56 @@ This repository includes configuration for a development container for working w ![Image of Remote - Containers extension](https://microsoft.github.io/vscode-remote-release/images/remote-containers-extn.png) - > Note that the Remote - Containers extension requires the Visual Studio Code distribution of Code - OSS. See the [FAQ](https://aka.ms/vscode-remote/faq/license) for details. + > **Note:** The Remote - Containers extension requires the Visual Studio Code distribution of Code - OSS. See the [FAQ](https://aka.ms/vscode-remote/faq/license) for details. -4. Press Ctrl/Cmd + Shift + P and select **Remote - Containers: Open Repository in Container...**. +4. Press Ctrl/Cmd + Shift + P or F1 and select **Remote-Containers: Clone Repository in Container Volume...**. - > **Tip:** While you can use your local source tree instead, operations like `yarn install` can be slow on macOS or using the Hyper-V engine on Windows. We recommend the "open repository" approach instead since it uses "named volume" rather than the local filesystem. + > **Tip:** While you can use your local source tree instead, operations like `yarn install` can be slow on macOS or when using the Hyper-V engine on Windows. We recommend the "clone repository in container" approach instead since it uses "named volume" rather than the local filesystem. 5. Type `https://github.com/microsoft/vscode` (or a branch or PR URL) in the input box and press Enter. -6. After the container is running, open a web browser and go to [http://localhost:6080](http://localhost:6080) or use a [VNC Viewer](https://www.realvnc.com/en/connect/download/viewer/) to connect to `localhost:5901` and enter `vscode` as the password. +6. After the container is running, open a web browser and go to [http://localhost:6080](http://localhost:6080), or use a [VNC Viewer](https://www.realvnc.com/en/connect/download/viewer/) to connect to `localhost:5901` and enter `vscode` as the password. -Anything you start in VS Code or the integrated terminal will appear here. +Anything you start in VS Code, or the integrated terminal, will appear here. Next: **[Try it out!](#try-it)** ## Quick start - GitHub Codespaces -> **IMPORTANT:** You need to use a "Standard" sized codespace or larger (4-core, 8GB) since VS Code needs 6GB of RAM to compile. This is now the default for GitHub Codespaces, but do not downgrade to "Basic" unless you do not intend to compile. +1. From the [microsoft/vscode GitHub repository](https://github.com/microsoft/vscode), click on the **Code** dropdown, select **Open with Codespaces**, and then click on **New codespace**. If prompted, select the **Standard** machine size (which is also the default). -1. From the [microsoft/vscode GitHub repository](https://github.com/microsoft/vscode), click on the **Code** dropdown, select **Open with Codespaces**, and the **New codespace** + > **Note:** You will not see these options within GitHub if you are not in the Codespaces beta. - > Note that you will not see these options if you are not in the beta yet. +2. After the codespace is up and running in your browser, press Ctrl/Cmd + Shift + P or F1 and select **Ports: Focus on Ports View**. -2. After the codespace is up and running in your browser, press F1 and select **Ports: Focus on Ports View**. +3. You should see **VNC web client (6080)** under in the list of ports. Select the line and click on the globe icon to open it in a browser tab. -3. You should see port `6080` under **Forwarded Ports**. Select the line and click on the globe icon to open it in a browser tab. - - > If you do not see port `6080`, press F1, select **Forward a Port** and enter port `6080`. + > **Tip:** If you do not see the port, Ctrl/Cmd + Shift + P or F1, select **Forward a Port** and enter port `6080`. 4. In the new tab, you should see noVNC. Click **Connect** and enter `vscode` as the password. -Anything you start in VS Code or the integrated terminal will appear here. +Anything you start in VS Code, or the integrated terminal, will appear here. Next: **[Try it out!](#try-it)** ### Using VS Code with GitHub Codespaces -You will likely see better performance when accessing the codespace you created from VS Code since you can use a[VNC Viewer](https://www.realvnc.com/en/connect/download/viewer/). Here's how to do it. +You may see improved VNC responsiveness when accessing a codespace from VS Code client since you can use a [VNC Viewer](https://www.realvnc.com/en/connect/download/viewer/). Here's how to do it. -1. [Create a codespace](#quick-start---github-codespaces) if you have not already. +1. Install [Visual Studio Code Stable](https://code.visualstudio.com/) or [Insiders](https://code.visualstudio.com/insiders/) and the the [GitHub Codespaces extension](https://marketplace.visualstudio.com/items?itemName=GitHub.codespaces). -2. Set up [VS Code for use with GitHub Codespaces](https://docs.github.com/github/developing-online-with-codespaces/using-codespaces-in-visual-studio-code) + > **Note:** The GitHub Codespaces extension requires the Visual Studio Code distribution of Code - OSS. -3. After the VS Code is up and running, press F1, choose **Codespaces: Connect to Codespace**, and select the codespace you created. +2. After the VS Code is up and running, press Ctrl/Cmd + Shift + P or F1, choose **Codespaces: Create New Codespace**, and use the following settings: + - `microsoft/vscode` for the repository. + - Select any branch (e.g. **main**) - you select a different one later. + - Choose **Standard** (4-core, 8GB) as the size. -4. After you've connected to the codespace, use a [VNC Viewer](https://www.realvnc.com/en/connect/download/viewer/) to connect to `localhost:5901` and enter `vscode` as the password. +4. After you have connected to the codespace, you can use a [VNC Viewer](https://www.realvnc.com/en/connect/download/viewer/) to connect to `localhost:5901` and enter `vscode` as the password. -5. Anything you start in VS Code or the integrated terminal will appear here. + > **Tip:** You may also need change your VNC client's **Picture Quaility** setting to **High** to get a full color desktop. + +5. Anything you start in VS Code, or the integrated terminal, will appear here. Next: **[Try it out!](#try-it)** @@ -70,20 +73,18 @@ Next: **[Try it out!](#try-it)** This container uses the [Fluxbox](http://fluxbox.org/) window manager to keep things lean. **Right-click on the desktop** to see menu options. It works with GNOME and GTK applications, so other tools can be installed if needed. -Note you can also set the resolution from the command line by typing `set-resolution`. +> **Note:** You can also set the resolution from the command line by typing `set-resolution`. To start working with Code - OSS, follow these steps: -1. In your local VS Code, open a terminal (Ctrl/Cmd + Shift + \`) and type the following commands: +1. In your local VS Code client, open a terminal (Ctrl/Cmd + Shift + \`) and type the following commands: ```bash yarn install bash scripts/code.sh ``` - Note that a previous run of `yarn install` will already be cached, so this step should simply pick up any recent differences. - -2. After the build is complete, open a web browser or a [VNC Viewer](https://www.realvnc.com/en/connect/download/viewer/) to the desktop environnement as described in the quick start and enter `vscode` as the password. +2. After the build is complete, open a web browser or a [VNC Viewer](https://www.realvnc.com/en/connect/download/viewer/) to connect to the desktop environment as described in the quick start and enter `vscode` as the password. 3. You should now see Code - OSS! @@ -91,7 +92,7 @@ Next, let's try debugging. 1. Shut down Code - OSS by clicking the box in the upper right corner of the Code - OSS window through your browser or VNC viewer. -2. Go to your local VS Code client, and use Run / Debug view to launch the **VS Code** configuration. (Typically the default, so you can likely just press F5). +2. Go to your local VS Code client, and use the **Run / Debug** view to launch the **VS Code** configuration. (Typically the default, so you can likely just press F5). > **Note:** If launching times out, you can increase the value of `timeout` in the "VS Code", "Attach Main Process", "Attach Extension Host", and "Attach to Shared Process" configurations in [launch.json](../.vscode/launch.json). However, running `scripts/code.sh` first will set up Electron which will usually solve timeout issues. diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3b82cd9028..d66344eccf 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,20 +3,26 @@ // Image contents: https://github.com/microsoft/vscode-dev-containers/blob/master/repository-containers/images/github.com/microsoft/vscode/.devcontainer/base.Dockerfile "image": "mcr.microsoft.com/vscode/devcontainers/repos/microsoft/vscode:branch-main", - - "workspaceMount": "source=${localWorkspaceFolder},target=/home/node/workspace/vscode,type=bind,consistency=cached", - "workspaceFolder": "/home/node/workspace/vscode", "overrideCommand": false, "runArgs": [ "--init", "--security-opt", "seccomp=unconfined"], "settings": { - "terminal.integrated.shell.linux": "/bin/bash", "resmon.show.battery": false, "resmon.show.cpufreq": false }, - // noVNC, VNC, debug ports - "forwardPorts": [6080, 5901, 9222], + // noVNC, VNC + "forwardPorts": [6080, 5901], + "portsAttributes": { + "6080": { + "label": "VNC web client (noVNC)", + "onAutoForward": "silent" + }, + "5901": { + "label": "VNC TCP port", + "onAutoForward": "silent" + } + }, "extensions": [ "dbaeumer.vscode-eslint", diff --git a/.eslintrc.json b/.eslintrc.json index 4e75422915..dd9e34c2db 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -104,6 +104,7 @@ "restrictions": [ "assert", "sinon", + "sinon-test", "vs/nls", "**/{vs,sql}/base/common/**", "**/{vs,sql}/base/test/common/**" @@ -141,6 +142,7 @@ "restrictions": [ "assert", "sinon", + "sinon-test", "vs/nls", "**/{vs,sql}/base/{common,browser}/**", "**/{vs,sql}/base/test/{common,browser}/**", @@ -220,6 +222,7 @@ "assert", "typemoq", "sinon", + "sinon-test", "vs/nls", "azdata", "**/{vs,sql}/base/common/**", @@ -292,6 +295,7 @@ "typemoq", "sinon", "azdata", + "sinon-test", "vs/nls", "**/{vs,sql}/base/{common,browser}/**", "**/{vs,sql}/base/test/{common,browser}/**", @@ -315,6 +319,7 @@ "restrictions": [ "assert", "sinon", + "sinon-test", "vs/nls", "**/{vs,sql}/base/common/**", "**/{vs,sql}/platform/*/common/**", @@ -338,6 +343,7 @@ "restrictions": [ "assert", "sinon", + "sinon-test", "vs/nls", "**/{vs,sql}/base/{common,browser}/**", "**/{vs,sql}/platform/*/{common,browser}/**", @@ -361,6 +367,7 @@ "restrictions": [ "assert", "sinon", + "sinon-test", "vs/nls", "**/{vs,sql}/base/common/**", "**/{vs,sql}/platform/*/common/**", @@ -387,6 +394,7 @@ "restrictions": [ "assert", "sinon", + "sinon-test", "vs/nls", "**/{vs,sql}/base/{common,browser}/**", "**/{vs,sql}/platform/*/{common,browser}/**", @@ -401,6 +409,7 @@ "restrictions": [ "assert", "sinon", + "sinon-test", "vs/nls", "**/{vs,sql}/base/{common,browser}/**", "**/{vs,sql}/base/test/{common,browser}/**", @@ -523,7 +532,7 @@ "**/{vs,sql}/platform/**", "**/{vs,sql}/editor/**", "**/{vs,sql}/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", - "vs/workbench/contrib/files/common/editors/fileEditorInput", + "vs/workbench/contrib/files/browser/editors/fileEditorInput", "**/{vs,sql}/workbench/services/**", "**/{vs,sql}/workbench/test/**", "*" // node modules @@ -958,6 +967,7 @@ "**/{vs,sql}/**", "assert", "sinon", + "sinon-test", "crypto", "vscode", "typemoq", @@ -993,6 +1003,7 @@ "assert", "typemoq", "sinon", + "sinon-test", "crypto", "xterm*", "azdata" @@ -1005,6 +1016,7 @@ "assert", "typemoq", "sinon", + "sinon-test", "crypto", "xterm*" ] @@ -1042,6 +1054,7 @@ "vscode-dts-cancellation": "warn", "vscode-dts-use-thenable": "warn", "vscode-dts-region-comments": "warn", + "vscode-dts-vscode-in-comments": "warn", "vscode-dts-provider-naming": [ "warn", { diff --git a/.github/subscribers.json b/.github/subscribers.json index 7ee6e5cdad..25c676a47c 100644 --- a/.github/subscribers.json +++ b/.github/subscribers.json @@ -4,6 +4,7 @@ "rchiodo", "greazer", "donjayamanne", - "jilljac" + "jilljac", + "IanMatthewHuff" ] } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c74e2dce38..43e455c90a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -243,7 +243,6 @@ jobs: with: path: "**/node_modules" key: ${{ runner.os }}-cacheNodeModules13-${{ steps.nodeModulesCacheKey.outputs.value }} - restore-keys: ${{ runner.os }}-cacheNodeModules13- - name: Get yarn cache directory path id: yarnCacheDirPath if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} @@ -279,6 +278,9 @@ jobs: # - name: Run Monaco Editor Checks {{SQL CARBON EDIT}} Remove Monaco checks # run: yarn monaco-compile-check + - name: Compile /build/ + run: yarn --cwd build compile + - name: Run Trusted Types Checks run: yarn tsec-compile-check diff --git a/.vscode/notebooks/api.github-issues b/.vscode/notebooks/api.github-issues index b9e25a7c91..04d665a4f1 100644 --- a/.vscode/notebooks/api.github-issues +++ b/.vscode/notebooks/api.github-issues @@ -2,37 +2,31 @@ { "kind": 1, "language": "markdown", - "value": "#### Config", - "editable": true + "value": "#### Config" }, { "kind": 2, "language": "github-issues", - "value": "$repo=repo:microsoft/vscode\n$milestone=milestone:\"April 2021\"", - "editable": true + "value": "$repo=repo:microsoft/vscode\n$milestone=milestone:\"June 2021\"" }, { "kind": 1, "language": "markdown", - "value": "### Finalization", - "editable": true + "value": "### Finalization" }, { "kind": 2, "language": "github-issues", - "value": "$repo $milestone label:api-finalization", - "editable": true + "value": "$repo $milestone label:api-finalization" }, { "kind": 1, "language": "markdown", - "value": "### Proposals", - "editable": true + "value": "### Proposals" }, { "kind": 2, "language": "github-issues", - "value": "$repo $milestone is:open label:api-proposal ", - "editable": true + "value": "$repo $milestone is:open label:api-proposal " } ] \ No newline at end of file diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index 881af2c14b..bc2fba29dd 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/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 repo:microsoft/vscode-remotehub\n\n$MILESTONE=milestone:\"April 2021\"" + "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 repo:microsoft/vscode-remotehub\n\n$MILESTONE=milestone:\"May 2021\"" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index c435ee7750..aad3a8db3a 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/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-remotehub\n\n$MILESTONE=milestone:\"April 2021\"\n\n$MINE=assignee:@me" + "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-remotehub\n\n$MILESTONE=milestone:\"May 2021\"\n\n$MINE=assignee:@me" }, { "kind": 1, @@ -157,7 +157,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE -$MINE is:issue is:closed sort:updated-asc label:bug -label:verified -label:z-author-verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:verification-found -author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author: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" + "value": "$REPOS $MILESTONE -$MINE is:issue is:closed sort:updated-asc label:bug -label:verified -label:z-author-verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:verification-found -author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author: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 -author:hediet" }, { "kind": 1, diff --git a/.vscode/notebooks/my-work.github-issues b/.vscode/notebooks/my-work.github-issues index 4e288133b7..fe57793626 100644 --- a/.vscode/notebooks/my-work.github-issues +++ b/.vscode/notebooks/my-work.github-issues @@ -2,20 +2,17 @@ { "kind": 1, "language": "markdown", - "value": "##### `Config`: This should be changed every month/milestone", - "editable": true + "value": "##### `Config`: This should be changed every month/milestone" }, { "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:\"April 2021\"", - "editable": true + "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:\"June 2021\"" }, { "kind": 1, - "language": "github-issues", - "value": "## Milestone Work", - "editable": true + "language": "markdown", + "value": "## Milestone Work" }, { "kind": 2, @@ -25,57 +22,48 @@ }, { "kind": 1, - "language": "github-issues", - "value": "## Bugs, Debt, Features...", - "editable": true + "language": "markdown", + "value": "## Bugs, Debt, Features..." }, { "kind": 1, "language": "markdown", - "value": "#### My Bugs", - "editable": true + "value": "#### My Bugs" }, { "kind": 2, "language": "github-issues", - "value": "$repos assignee:@me is:open label:bug", - "editable": true + "value": "$repos assignee:@me is:open label:bug" }, { "kind": 1, "language": "markdown", - "value": "#### Debt & Engineering", - "editable": true + "value": "#### Debt & Engineering" }, { "kind": 2, "language": "github-issues", - "value": "$repos assignee:@me is:open label:debt OR $repos assignee:@me is:open label:engineering", - "editable": true + "value": "$repos assignee:@me is:open label:debt OR $repos assignee:@me is:open label:engineering" }, { "kind": 1, "language": "markdown", - "value": "#### Performance 🐌 🔜 🏎", - "editable": true + "value": "#### Performance 🐌 🔜 🏎" }, { "kind": 2, "language": "github-issues", - "value": "$repos assignee:@me is:open label:perf OR $repos assignee:@me is:open label:perf-startup OR $repos assignee:@me is:open label:perf-bloat OR $repos assignee:@me is:open label:freeze-slow-crash-leak", - "editable": true + "value": "$repos assignee:@me is:open label:perf OR $repos assignee:@me is:open label:perf-startup OR $repos assignee:@me is:open label:perf-bloat OR $repos assignee:@me is:open label:freeze-slow-crash-leak" }, { "kind": 1, "language": "markdown", - "value": "#### Feature Requests", - "editable": true + "value": "#### Feature Requests" }, { "kind": 2, "language": "github-issues", - "value": "$repos assignee:@me is:open label:feature-request milestone:Backlog sort:reactions-+1-desc", - "editable": true + "value": "$repos assignee:@me is:open label:feature-request milestone:Backlog sort:reactions-+1-desc" }, { "kind": 2, @@ -86,26 +74,22 @@ { "kind": 1, "language": "markdown", - "value": "### Personal Inbox\n", - "editable": true + "value": "### Personal Inbox\n" }, { "kind": 1, "language": "markdown", - "value": "\n#### Missing Type label", - "editable": true + "value": "\n#### Missing Type label" }, { "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 + "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" }, { "kind": 1, "language": "markdown", - "value": "#### Not Actionable", - "editable": true + "value": "#### Not Actionable" }, { "kind": 2, diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 86cfa7a44c..639c9fe520 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -55,39 +55,11 @@ } } }, - { - "type": "npm", - "script": "watch-extension-mediad", - "label": "Ext Media - Build", - "isBackground": true, - "presentation": { - "reveal": "never", - "group": "buildWatchers" - }, - "problemMatcher": { - "owner": "typescript", - "applyTo": "closedDocuments", - "fileLocation": [ - "absolute" - ], - "pattern": { - "regexp": "Error: ([^(]+)\\((\\d+|\\d+,\\d+|\\d+,\\d+,\\d+,\\d+)\\): (.*)$", - "file": 1, - "location": 2, - "message": 3 - }, - "background": { - "beginsPattern": "Starting compilation", - "endsPattern": "Finished compilation" - } - } - }, { "label": "VS Code - Build", "dependsOn": [ "Core - Build", - "Ext - Build", - "Ext Media - Build", + "Ext - Build" ], "group": { "kind": "build", @@ -102,7 +74,8 @@ "group": "build", "presentation": { "reveal": "never", - "group": "buildKillers" + "group": "buildKillers", + "close": true }, "problemMatcher": "$tsc" }, @@ -113,18 +86,8 @@ "group": "build", "presentation": { "reveal": "never", - "group": "buildKillers" - }, - "problemMatcher": "$tsc" - }, - { - "type": "npm", - "script": "kill-watch-extension-mediad", - "label": "Kill Ext Media - Build", - "group": "build", - "presentation": { - "reveal": "never", - "group": "buildKillers" + "group": "buildKillers", + "close": true }, "problemMatcher": "$tsc" }, @@ -132,8 +95,7 @@ "label": "Kill VS Code - Build", "dependsOn": [ "Kill Core - Build", - "Kill Ext - Build", - "Kill Ext Media - Build", + "Kill Ext - Build" ], "group": "build", "problemMatcher": [] @@ -252,7 +214,8 @@ "command": "node build/lib/preLaunch.js", "label": "Ensure Prelaunch Dependencies", "presentation": { - "reveal": "silent" + "reveal": "silent", + "close": true } }, { diff --git a/.yarnrc b/.yarnrc index 0b7e220665..ba29080966 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,3 +1,3 @@ disturl "https://electronjs.org/headers" -target "12.0.7" +target "12.0.9" runtime "electron" diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 218692880c..05bbfe95b6 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -86,6 +86,125 @@ expressly granted herein, whether by implication, estoppel or otherwise. Microsoft PROSE SDK: https://microsoft.github.io/prose + atom/language-clojure version 0.22.7 (https://github.com/atom/language-clojure) + atom/language-coffee-script version 0.49.3 (https://github.com/atom/language-coffee-script) + atom/language-css version 0.44.4 (https://github.com/atom/language-css) + atom/language-java version 0.32.1 (https://github.com/atom/language-java) + atom/language-sass version 0.62.1 (https://github.com/atom/language-sass) + atom/language-shellscript version 0.26.0 (https://github.com/atom/language-shellscript) + atom/language-xml version 0.35.2 (https://github.com/atom/language-xml) + better-go-syntax version 1.0.0 (https://github.com/jeff-hykin/better-go-syntax/ ) + Colorsublime-Themes version 0.1.0 (https://github.com/Colorsublime/Colorsublime-Themes) + daaain/Handlebars version 1.8.0 (https://github.com/daaain/Handlebars) + dart-lang/dart-syntax-highlight (https://github.com/dart-lang/dart-syntax-highlight) + davidrios/pug-tmbundle (https://github.com/davidrios/pug-tmbundle) + definitelytyped (https://github.com/DefinitelyTyped/DefinitelyTyped) + demyte/language-cshtml version 0.3.0 (https://github.com/demyte/language-cshtml) + Document Object Model version 4.0.0 (https://www.w3.org/DOM/) + dotnet/csharp-tmLanguage version 0.1.0 (https://github.com/dotnet/csharp-tmLanguage) + expand-abbreviation version 0.5.8 (https://github.com/emmetio/expand-abbreviation) + fadeevab/make.tmbundle (https://github.com/fadeevab/make.tmbundle) + freebroccolo/atom-language-swift (https://github.com/freebroccolo/atom-language-swift) + HTML 5.1 W3C Working Draft version 08 October 2015 (http://www.w3.org/TR/2015/WD-html51-20151008/) + Ikuyadeu/vscode-R version 1.3.0 (https://github.com/Ikuyadeu/vscode-R) + insane version 2.6.2 (https://github.com/bevacqua/insane) + Ionic documentation version 1.2.4 (https://github.com/ionic-team/ionic-site) + ionide/ionide-fsgrammar (https://github.com/ionide/ionide-fsgrammar) + jeff-hykin/cpp-textmate-grammar version 1.12.11 (https://github.com/jeff-hykin/cpp-textmate-grammar) + jeff-hykin/cpp-textmate-grammar version 1.15.5 (https://github.com/jeff-hykin/cpp-textmate-grammar) + js-beautify version 1.6.8 (https://github.com/beautify-web/js-beautify) + JuliaEditorSupport/atom-language-julia version 0.21.0 (https://github.com/JuliaEditorSupport/atom-language-julia) + Jxck/assert version 1.0.0 (https://github.com/Jxck/assert) + language-docker (https://github.com/moby/moby) + language-less version 0.34.2 (https://github.com/atom/language-less) + language-php version 0.46.2 (https://github.com/atom/language-php) + MagicStack/MagicPython version 1.1.1 (https://github.com/MagicStack/MagicPython) + marked version 1.1.0 (https://github.com/markedjs/marked) + mdn-data version 1.1.12 (https://github.com/mdn/data) + microsoft/TypeScript-TmLanguage version 0.0.1 (https://github.com/microsoft/TypeScript-TmLanguage) + microsoft/vscode-JSON.tmLanguage (https://github.com/microsoft/vscode-JSON.tmLanguage) + microsoft/vscode-markdown-tm-grammar version 1.0.0 (https://github.com/microsoft/vscode-markdown-tm-grammar) + microsoft/vscode-mssql version 1.9.0 (https://github.com/microsoft/vscode-mssql) + mmims/language-batchfile version 0.7.6 (https://github.com/mmims/language-batchfile) + NVIDIA/cuda-cpp-grammar (https://github.com/NVIDIA/cuda-cpp-grammar) + PowerShell/EditorSyntax version 1.0.0 (https://github.com/PowerShell/EditorSyntax) + rust-syntax version 0.4.3 (https://github.com/dustypomerleau/rust-syntax) + seti-ui version 0.1.0 (https://github.com/jesseweed/seti-ui) + shaders-tmLanguage version 0.1.0 (https://github.com/tgjones/shaders-tmLanguage) + textmate/asp.vb.net.tmbundle (https://github.com/textmate/asp.vb.net.tmbundle) + textmate/c.tmbundle (https://github.com/textmate/c.tmbundle) + textmate/diff.tmbundle (https://github.com/textmate/diff.tmbundle) + textmate/git.tmbundle (https://github.com/textmate/git.tmbundle) + textmate/groovy.tmbundle (https://github.com/textmate/groovy.tmbundle) + textmate/html.tmbundle (https://github.com/textmate/html.tmbundle) + textmate/ini.tmbundle (https://github.com/textmate/ini.tmbundle) + textmate/javascript.tmbundle (https://github.com/textmate/javascript.tmbundle) + textmate/lua.tmbundle (https://github.com/textmate/lua.tmbundle) + textmate/markdown.tmbundle (https://github.com/textmate/markdown.tmbundle) + textmate/perl.tmbundle (https://github.com/textmate/perl.tmbundle) + textmate/ruby.tmbundle (https://github.com/textmate/ruby.tmbundle) + textmate/yaml.tmbundle (https://github.com/textmate/yaml.tmbundle) + TypeScript-TmLanguage version 0.1.8 (https://github.com/microsoft/TypeScript-TmLanguage) + TypeScript-TmLanguage version 1.0.0 (https://github.com/microsoft/TypeScript-TmLanguage) + Unicode version 12.0.0 (https://home.unicode.org/) + vscode-codicons version 0.0.14 (https://github.com/microsoft/vscode-codicons) + vscode-logfile-highlighter version 2.11.0 (https://github.com/emilast/vscode-logfile-highlighter) + vscode-swift version 0.0.1 (https://github.com/owensd/vscode-swift) + Web Background Synchronization (https://github.com/WICG/background-sync) + + +%% atom/language-clojure NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2014 GitHub Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +This package was derived from a TextMate bundle located at +https://github.com/mmcgrana/textmate-clojure and distributed under the +following license, located in `LICENSE.md`: + +The MIT License (MIT) + +Copyright (c) 2010- Mark McGranaghan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF atom/language-clojure NOTICES AND INFORMATION + %% angular NOTICES AND INFORMATION BEGIN HERE Copyright (c) 2014-2017 Google, Inc. http://angular.io @@ -560,6 +679,63 @@ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ========================================= END OF http-proxy-agent NOTICES AND INFORMATION +%% dart-lang/dart-syntax-highlight NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright 2020, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================= +END OF dart-lang/dart-syntax-highlight NOTICES AND INFORMATION + +%% davidrios/pug-tmbundle NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2016 David Rios + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF davidrios/pug-tmbundle NOTICES AND INFORMATION + %% iconv-lite NOTICES AND INFORMATION BEGIN HERE ========================================= Copyright (c) 2011 Alexander Shtuchkin @@ -1486,6 +1662,61 @@ THE SOFTWARE. ========================================= END OF node-pty NOTICES AND INFORMATION +%% JuliaEditorSupport/atom-language-julia NOTICES AND INFORMATION BEGIN HERE +========================================= +The atom-language-julia package is licensed under the MIT "Expat" License: + +> Copyright (c) 2015 +> +> Permission is hereby granted, free of charge, to any person obtaining +> a copy of this software and associated documentation files (the +> "Software"), to deal in the Software without restriction, including +> without limitation the rights to use, copy, modify, merge, publish, +> distribute, sublicense, and/or sell copies of the Software, and to +> permit persons to whom the Software is furnished to do so, subject to +> the following conditions: +> +> The above copyright notice and this permission notice shall be +> included in all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +> MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +> IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +> CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +> TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +> SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF JuliaEditorSupport/atom-language-julia NOTICES AND INFORMATION + +%% Jxck/assert NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2011 Jxck + +Originally from node.js (http://nodejs.org) +Copyright Joyent, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF Jxck/assert NOTICES AND INFORMATION + %% nsfw NOTICES AND INFORMATION BEGIN HERE ========================================= The MIT License (MIT) diff --git a/build/.cachesalt b/build/.cachesalt index 013244143e..4ec7190dfc 100644 --- a/build/.cachesalt +++ b/build/.cachesalt @@ -1 +1 @@ -2021-04-07T03:52:18.011Z +2021-08-23T03:52:18.011Z diff --git a/build/azure-pipelines/common/createAsset.js b/build/azure-pipelines/common/createAsset.js index c972cdf3c2..d197cf7c25 100644 --- a/build/azure-pipelines/common/createAsset.js +++ b/build/azure-pipelines/common/createAsset.js @@ -5,15 +5,101 @@ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); const fs = require("fs"); +const url = require("url"); 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'); +if (process.argv.length !== 8) { + console.error('Usage: node createAsset.js PRODUCT OS ARCH TYPE NAME FILE'); process.exit(-1); } +// Contains all of the logic for mapping details to our actual product names in CosmosDB +function getPlatform(product, os, arch, type) { + switch (os) { + case 'win32': + switch (product) { + case 'client': + const asset = arch === 'ia32' ? 'win32' : `win32-${arch}`; + switch (type) { + case 'archive': + return `${asset}-archive`; + case 'setup': + return asset; + case 'user-setup': + return `${asset}-user`; + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + case 'server': + if (arch === 'arm64') { + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + return arch === 'ia32' ? 'server-win32' : `server-win32-${arch}`; + case 'web': + if (arch === 'arm64') { + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + return arch === 'ia32' ? 'server-win32-web' : `server-win32-${arch}-web`; + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + case 'linux': + switch (type) { + case 'snap': + return `linux-snap-${arch}`; + case 'archive-unsigned': + switch (product) { + case 'client': + return `linux-${arch}`; + case 'server': + return `server-linux-${arch}`; + case 'web': + return arch === 'standalone' ? 'web-standalone' : `server-linux-${arch}-web`; + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + case 'deb-package': + return `linux-deb-${arch}`; + case 'rpm-package': + return `linux-rpm-${arch}`; + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + case 'darwin': + switch (product) { + case 'client': + if (arch === 'x64') { + return 'darwin'; + } + return `darwin-${arch}`; + case 'server': + return 'server-darwin'; + case 'web': + if (arch !== 'x64') { + throw `What should the platform be?: ${product} ${os} ${arch} ${type}`; + } + return 'server-darwin-web'; + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } +} +// Contains all of the logic for mapping types to our actual types in CosmosDB +function getRealType(type) { + switch (type) { + case 'user-setup': + return 'setup'; + case 'deb-package': + case 'rpm-package': + return 'package'; + default: + return type; + } +} function hashStream(hashName, stream) { return new Promise((c, e) => { const shasum = crypto.createHash(hashName); @@ -45,7 +131,10 @@ function getEnv(name) { return result; } async function main() { - const [, , platform, type, fileName, filePath] = process.argv; + const [, , product, os, arch, unprocessedType, fileName, filePath] = process.argv; + // getPlatform needs the unprocessedType + const platform = getPlatform(product, os, arch, unprocessedType); + const type = getRealType(unprocessedType); const quality = getEnv('VSCODE_QUALITY'); const commit = getEnv('BUILD_SOURCEVERSION'); console.log('Creating asset...'); @@ -65,14 +154,27 @@ async function main() { 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); + 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; + console.log('Uploading blobs to Azure storage and Mooncake Azure storage...'); + await retry_1.retry(() => Promise.all([ + uploadBlob(blobService, quality, blobName, filePath, fileName), + uploadBlob(mooncakeBlobService, quality, blobName, filePath, fileName) + ])); console.log('Blobs successfully uploaded.'); + // TODO: Understand if blobName and blobPath are the same and replace blobPath with blobName if so. + const assetUrl = `${process.env['AZURE_CDN_URL']}/${quality}/${blobName}`; + const blobPath = url.parse(assetUrl).path; + const mooncakeUrl = `${process.env['MOONCAKE_CDN_URL']}${blobPath}`; const asset = { platform, type, - url: `${process.env['AZURE_CDN_URL']}/${quality}/${blobName}`, + url: assetUrl, hash: sha1hash, + mooncakeUrl, sha256hash, size }; @@ -83,7 +185,8 @@ async function main() { 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 (0, retry_1.retry)(() => scripts.storedProcedure('createAsset').execute('', [commit, asset, true])); + await retry_1.retry(() => scripts.storedProcedure('createAsset').execute('', [commit, asset, true])); + console.log(` Done ✔️`); } main().then(() => { console.log('Asset successfully created'); diff --git a/build/azure-pipelines/common/createAsset.ts b/build/azure-pipelines/common/createAsset.ts index 4fee172297..37a49bd237 100644 --- a/build/azure-pipelines/common/createAsset.ts +++ b/build/azure-pipelines/common/createAsset.ts @@ -6,6 +6,7 @@ 'use strict'; import * as fs from 'fs'; +import * as url from 'url'; import { Readable } from 'stream'; import * as crypto from 'crypto'; import * as azure from 'azure-storage'; @@ -24,11 +25,98 @@ interface Asset { supportsFastUpdate?: boolean; } -if (process.argv.length !== 6) { - console.error('Usage: node createAsset.js PLATFORM TYPE NAME FILE'); +if (process.argv.length !== 8) { + console.error('Usage: node createAsset.js PRODUCT OS ARCH TYPE NAME FILE'); process.exit(-1); } +// Contains all of the logic for mapping details to our actual product names in CosmosDB +function getPlatform(product: string, os: string, arch: string, type: string): string { + switch (os) { + case 'win32': + switch (product) { + case 'client': + const asset = arch === 'ia32' ? 'win32' : `win32-${arch}`; + switch (type) { + case 'archive': + return `${asset}-archive`; + case 'setup': + return asset; + case 'user-setup': + return `${asset}-user`; + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + case 'server': + if (arch === 'arm64') { + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + return arch === 'ia32' ? 'server-win32' : `server-win32-${arch}`; + case 'web': + if (arch === 'arm64') { + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + return arch === 'ia32' ? 'server-win32-web' : `server-win32-${arch}-web`; + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + case 'linux': + switch (type) { + case 'snap': + return `linux-snap-${arch}`; + case 'archive-unsigned': + switch (product) { + case 'client': + return `linux-${arch}`; + case 'server': + return `server-linux-${arch}`; + case 'web': + return arch === 'standalone' ? 'web-standalone' : `server-linux-${arch}-web`; + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + case 'deb-package': + return `linux-deb-${arch}`; + case 'rpm-package': + return `linux-rpm-${arch}`; + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + case 'darwin': + switch (product) { + case 'client': + if (arch === 'x64') { + return 'darwin'; + } + return `darwin-${arch}`; + case 'server': + return 'server-darwin'; + case 'web': + if (arch !== 'x64') { + throw `What should the platform be?: ${product} ${os} ${arch} ${type}`; + } + return 'server-darwin-web'; + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } +} + +// Contains all of the logic for mapping types to our actual types in CosmosDB +function getRealType(type: string) { + switch (type) { + case 'user-setup': + return 'setup'; + case 'deb-package': + case 'rpm-package': + return 'package'; + default: + return type; + } +} + function hashStream(hashName: string, stream: Readable): Promise { return new Promise((c, e) => { const shasum = crypto.createHash(hashName); @@ -68,7 +156,10 @@ function getEnv(name: string): string { } async function main(): Promise { - const [, , platform, type, fileName, filePath] = process.argv; + const [, , product, os, arch, unprocessedType, fileName, filePath] = process.argv; + // getPlatform needs the unprocessedType + const platform = getPlatform(product, os, arch, unprocessedType); + const type = getRealType(unprocessedType); const quality = getEnv('VSCODE_QUALITY'); const commit = getEnv('BUILD_SOURCEVERSION'); @@ -98,17 +189,33 @@ async function main(): Promise { return; } - console.log('Uploading blobs to Azure storage...'); + const mooncakeBlobService = azure.createBlobService(storageAccount, process.env['MOONCAKE_STORAGE_ACCESS_KEY']!, `${storageAccount}.blob.core.chinacloudapi.cn`) + .withFilter(new azure.ExponentialRetryPolicyFilter(20)); - await uploadBlob(blobService, quality, blobName, filePath, fileName); + // mooncake is fussy and far away, this is needed! + blobService.defaultClientRequestTimeoutInMs = 10 * 60 * 1000; + mooncakeBlobService.defaultClientRequestTimeoutInMs = 10 * 60 * 1000; + + console.log('Uploading blobs to Azure storage and Mooncake Azure storage...'); + + await retry(() => Promise.all([ + uploadBlob(blobService, quality, blobName, filePath, fileName), + uploadBlob(mooncakeBlobService, quality, blobName, filePath, fileName) + ])); console.log('Blobs successfully uploaded.'); + // TODO: Understand if blobName and blobPath are the same and replace blobPath with blobName if so. + const assetUrl = `${process.env['AZURE_CDN_URL']}/${quality}/${blobName}`; + const blobPath = url.parse(assetUrl).path; + const mooncakeUrl = `${process.env['MOONCAKE_CDN_URL']}${blobPath}`; + const asset: Asset = { platform, type, - url: `${process.env['AZURE_CDN_URL']}/${quality}/${blobName}`, + url: assetUrl, hash: sha1hash, + mooncakeUrl, sha256hash, size }; @@ -123,6 +230,8 @@ 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 retry(() => scripts.storedProcedure('createAsset').execute('', [commit, asset, true])); + + console.log(` Done ✔️`); } main().then(() => { diff --git a/build/azure-pipelines/common/createBuild.js b/build/azure-pipelines/common/createBuild.js index 15e06b1331..2165a62b8c 100644 --- a/build/azure-pipelines/common/createBuild.js +++ b/build/azure-pipelines/common/createBuild.js @@ -40,7 +40,7 @@ async function main() { }; 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 (0, retry_1.retry)(() => scripts.storedProcedure('createBuild').execute('', [Object.assign(Object.assign({}, build), { _partitionKey: '' })])); + await retry_1.retry(() => scripts.storedProcedure('createBuild').execute('', [Object.assign(Object.assign({}, build), { _partitionKey: '' })])); } main().then(() => { console.log('Build successfully created'); diff --git a/build/azure-pipelines/common/extract-telemetry.sh b/build/azure-pipelines/common/extract-telemetry.sh index 4abade1e7b..9cebe22bfd 100755 --- a/build/azure-pipelines/common/extract-telemetry.sh +++ b/build/azure-pipelines/common/extract-telemetry.sh @@ -4,12 +4,12 @@ set -e cd $BUILD_STAGINGDIRECTORY mkdir extraction cd extraction -git clone --depth 1 https://github.com/Microsoft/vscode-extension-telemetry.git -git clone --depth 1 https://github.com/Microsoft/vscode-chrome-debug-core.git -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 +git clone --depth 1 https://github.com/microsoft/vscode-extension-telemetry.git +git clone --depth 1 https://github.com/microsoft/vscode-chrome-debug-core.git +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/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 diff --git a/build/azure-pipelines/common/publish-webview.js b/build/azure-pipelines/common/publish-webview.js index f50e50277d..bf0c3d30c0 100644 --- a/build/azure-pipelines/common/publish-webview.js +++ b/build/azure-pipelines/common/publish-webview.js @@ -39,7 +39,7 @@ async function publish(commit, files) { .withFilter(new azure.ExponentialRetryPolicyFilter(20)); await assertContainer(blobService, commit); for (const file of files) { - const blobName = (0, path_1.basename)(file); + 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.`); @@ -58,7 +58,7 @@ function main() { } const opts = minimist(process.argv.slice(2)); const [directory] = opts._; - const files = fileNames.map(fileName => (0, path_1.join)(directory, fileName)); + const files = fileNames.map(fileName => path_1.join(directory, fileName)); publish(commit, files).catch(err => { console.error(err); process.exit(1); diff --git a/build/azure-pipelines/common/releaseBuild.js b/build/azure-pipelines/common/releaseBuild.js index ef44e03189..6932aed3bd 100644 --- a/build/azure-pipelines/common/releaseBuild.js +++ b/build/azure-pipelines/common/releaseBuild.js @@ -39,7 +39,7 @@ async function main() { } console.log(`Releasing build ${commit}...`); const scripts = client.database('builds').container(quality).scripts; - await (0, retry_1.retry)(() => scripts.storedProcedure('releaseBuild').execute('', [commit])); + await retry_1.retry(() => scripts.storedProcedure('releaseBuild').execute('', [commit])); } main().then(() => { console.log('Build successfully released'); diff --git a/build/azure-pipelines/common/sync-mooncake.js b/build/azure-pipelines/common/sync-mooncake.js deleted file mode 100644 index cf7c41e57b..0000000000 --- a/build/azure-pipelines/common/sync-mooncake.js +++ /dev/null @@ -1,87 +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'; -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 (0, 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 deleted file mode 100644 index aa645a8861..0000000000 --- a/build/azure-pipelines/common/sync-mooncake.ts +++ /dev/null @@ -1,131 +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'; - -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]); -} - -function error(...args: any[]) { - console.error(...[`[${new Date().toISOString()}]`, ...args]); -} - -if (process.argv.length < 3) { - error('Usage: node sync-mooncake.js '); - process.exit(-1); -} - -interface Build { - assets: Asset[]; -} - -interface Asset { - platform: string; - type: string; - url: string; - mooncakeUrl: string; - hash: string; - sha256hash: string; - size: number; - supportsFastUpdate?: boolean; -} - -async function sync(commit: string, quality: string): Promise { - log(`Synchronizing Mooncake assets for ${quality}, ${commit}...`); - - const client = new 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: azure.BlobService.CreateBlockBlobRequestOptions = { - 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(() => container.scripts.storedProcedure('setAssetMooncakeUrl') - .execute('', [commit, asset.platform, asset.type, mooncakeUrl])); - - log(` Done ✔️`); - } catch (err) { - error(err); - } - } - - log(`All done ✔️`); -} - -function main(): void { - 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/darwin/product-build-darwin-sign.yml b/build/azure-pipelines/darwin/product-build-darwin-sign.yml index 4ad8349c51..49f74b55c9 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-sign.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-sign.yml @@ -35,13 +35,13 @@ steps: displayName: Restore modules for just build folder and compile it - download: current - artifact: vscode-darwin-$(VSCODE_ARCH) + artifact: unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive displayName: Download $(VSCODE_ARCH) artifact - script: | set -e - unzip $(Pipeline.Workspace)/vscode-darwin-$(VSCODE_ARCH)/VSCode-darwin-$(VSCODE_ARCH).zip -d $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) - mv $(Pipeline.Workspace)/vscode-darwin-$(VSCODE_ARCH)/VSCode-darwin-$(VSCODE_ARCH).zip $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH).zip + unzip $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip -d $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) + mv $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH).zip displayName: Unzip & move - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 @@ -108,22 +108,18 @@ steps: condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'arm64')) - script: | - set -e - # For legacy purposes, arch for x64 is just 'darwin' case $VSCODE_ARCH in x64) ASSET_ID="darwin" ;; arm64) ASSET_ID="darwin-arm64" ;; universal) ASSET_ID="darwin-universal" ;; esac + echo "##vso[task.setvariable variable=ASSET_ID]$ASSET_ID" + displayName: Set asset id variable - VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ - AZURE_DOCUMENTDB_MASTERKEY="$(builds-docdb-key-readwrite)" \ - AZURE_STORAGE_ACCESS_KEY="$(ticino-storage-key)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(vscode-storage-key)" \ - node build/azure-pipelines/common/createAsset.js \ - "$ASSET_ID" \ - archive \ - "VSCode-$ASSET_ID.zip" \ - ../VSCode-darwin-$(VSCODE_ARCH).zip - displayName: Publish Clients + - script: mv $(agent.builddirectory)/VSCode-darwin-x64.zip $(agent.builddirectory)/VSCode-darwin.zip + displayName: Rename x64 build to it's legacy name + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64')) + + - publish: $(Agent.BuildDirectory)/VSCode-$(ASSET_ID).zip + artifact: vscode_client_darwin_$(VSCODE_ARCH)_archive diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 186920fe96..566eeb8052 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -138,19 +138,19 @@ steps: condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'universal'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - download: current - artifact: vscode-darwin-x64 + artifact: unsigned_vscode_client_darwin_x64_archive displayName: Download x64 artifact condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'universal')) - download: current - artifact: vscode-darwin-arm64 + artifact: unsigned_vscode_client_darwin_arm64_archive displayName: Download arm64 artifact condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'universal')) - script: | set -e - cp $(Pipeline.Workspace)/vscode-darwin-x64/VSCode-darwin-x64.zip $(agent.builddirectory)/VSCode-darwin-x64.zip - cp $(Pipeline.Workspace)/vscode-darwin-arm64/VSCode-darwin-arm64.zip $(agent.builddirectory)/VSCode-darwin-arm64.zip + cp $(Pipeline.Workspace)/unsigned_vscode_client_darwin_x64_archive/VSCode-darwin-x64.zip $(agent.builddirectory)/VSCode-darwin-x64.zip + cp $(Pipeline.Workspace)/unsigned_vscode_client_darwin_arm64_archive/VSCode-darwin-arm64.zip $(agent.builddirectory)/VSCode-darwin-arm64.zip unzip $(agent.builddirectory)/VSCode-darwin-x64.zip -d $(agent.builddirectory)/VSCode-darwin-x64 unzip $(agent.builddirectory)/VSCode-darwin-arm64.zip -d $(agent.builddirectory)/VSCode-darwin-arm64 DEBUG=* node build/darwin/create-universal-app.js @@ -280,26 +280,27 @@ steps: - script: | set -e - VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ - AZURE_DOCUMENTDB_MASTERKEY="$(builds-docdb-key-readwrite)" \ - AZURE_STORAGE_ACCESS_KEY="$(ticino-storage-key)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(vscode-storage-key)" \ - VSCODE_ARCH="$(VSCODE_ARCH)" ./build/azure-pipelines/darwin/publish-server.sh - displayName: Publish Servers + + # package Remote Extension Host + pushd .. && mv vscode-reh-darwin vscode-server-darwin && zip -Xry vscode-server-darwin.zip vscode-server-darwin && popd + + # package Remote Extension Host (Web) + pushd .. && mv vscode-reh-web-darwin vscode-server-darwin-web && zip -Xry vscode-server-darwin-web.zip vscode-server-darwin-web && popd + displayName: Prepare to publish servers condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), ne(variables['VSCODE_PUBLISH'], 'false')) - publish: $(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH).zip - artifact: vscode-darwin-$(VSCODE_ARCH) + artifact: unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive displayName: Publish client archive condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - publish: $(Agent.BuildDirectory)/vscode-server-darwin.zip - artifact: vscode-server-darwin-$(VSCODE_ARCH) + artifact: vscode_server_darwin_$(VSCODE_ARCH)_archive-unsigned 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 + artifact: vscode_web_darwin_$(VSCODE_ARCH)_archive-unsigned displayName: Publish web server archive condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), ne(variables['VSCODE_PUBLISH'], 'false')) @@ -308,5 +309,5 @@ steps: 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 diff --git a/build/azure-pipelines/darwin/publish-server.sh b/build/azure-pipelines/darwin/publish-server.sh deleted file mode 100755 index 72a85942d5..0000000000 --- a/build/azure-pipelines/darwin/publish-server.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -set -e - -if [ "$VSCODE_ARCH" == "x64" ]; then - # package Remote Extension Host - pushd .. && mv vscode-reh-darwin vscode-server-darwin && zip -Xry vscode-server-darwin.zip vscode-server-darwin && popd - - # publish Remote Extension Host - node build/azure-pipelines/common/createAsset.js \ - server-darwin \ - archive-unsigned \ - "vscode-server-darwin.zip" \ - ../vscode-server-darwin.zip -fi diff --git a/build/azure-pipelines/linux/alpine/publish.sh b/build/azure-pipelines/linux/alpine/publish.sh deleted file mode 100755 index 2f5647d1ea..0000000000 --- a/build/azure-pipelines/linux/alpine/publish.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -set -e -REPO="$(pwd)" -ROOT="$REPO/.." - -PLATFORM_LINUX="linux-alpine" - -# Publish Remote Extension Host -LEGACY_SERVER_BUILD_NAME="vscode-reh-$PLATFORM_LINUX" -SERVER_BUILD_NAME="vscode-server-$PLATFORM_LINUX" -SERVER_TARBALL_FILENAME="vscode-server-$PLATFORM_LINUX.tar.gz" -SERVER_TARBALL_PATH="$ROOT/$SERVER_TARBALL_FILENAME" - -rm -rf $ROOT/vscode-server-*.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" 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" diff --git a/build/azure-pipelines/linux/publish.sh b/build/azure-pipelines/linux/prepare-publish.sh similarity index 79% rename from build/azure-pipelines/linux/publish.sh rename to build/azure-pipelines/linux/prepare-publish.sh index 6d748c6e34..891fa8024e 100755 --- a/build/azure-pipelines/linux/publish.sh +++ b/build/azure-pipelines/linux/prepare-publish.sh @@ -13,8 +13,6 @@ TARBALL_PATH="$ROOT/$TARBALL_FILENAME" rm -rf $ROOT/code-*.tar.* (cd $ROOT && tar -czf $TARBALL_PATH $BUILDNAME) -node build/azure-pipelines/common/createAsset.js "$PLATFORM_LINUX" archive-unsigned "$TARBALL_FILENAME" "$TARBALL_PATH" - # Publish Remote Extension Host LEGACY_SERVER_BUILD_NAME="vscode-reh-$PLATFORM_LINUX" SERVER_BUILD_NAME="vscode-server-$PLATFORM_LINUX" @@ -24,8 +22,6 @@ SERVER_TARBALL_PATH="$ROOT/$SERVER_TARBALL_FILENAME" rm -rf $ROOT/vscode-server-*.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" 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" @@ -35,8 +31,6 @@ 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" ;; @@ -47,8 +41,6 @@ PLATFORM_DEB="linux-deb-$VSCODE_ARCH" DEB_FILENAME="$(ls $REPO/.build/linux/deb/$DEB_ARCH/deb/)" DEB_PATH="$REPO/.build/linux/deb/$DEB_ARCH/deb/$DEB_FILENAME" -node build/azure-pipelines/common/createAsset.js "$PLATFORM_DEB" package "$DEB_FILENAME" "$DEB_PATH" - # Publish RPM case $VSCODE_ARCH in x64) RPM_ARCH="x86_64" ;; @@ -61,8 +53,6 @@ PLATFORM_RPM="linux-rpm-$VSCODE_ARCH" RPM_FILENAME="$(ls $REPO/.build/linux/rpm/$RPM_ARCH/ | grep .rpm)" RPM_PATH="$REPO/.build/linux/rpm/$RPM_ARCH/$RPM_FILENAME" -node build/azure-pipelines/common/createAsset.js "$PLATFORM_RPM" package "$RPM_FILENAME" "$RPM_PATH" - # Publish Snap # Pack snap tarball artifact, in order to preserve file perms mkdir -p $REPO/.build/linux/snap-tarball @@ -73,3 +63,4 @@ rm -rf $SNAP_TARBALL_PATH # Export DEB_PATH, RPM_PATH echo "##vso[task.setvariable variable=DEB_PATH]$DEB_PATH" echo "##vso[task.setvariable variable=RPM_PATH]$RPM_PATH" +echo "##vso[task.setvariable variable=TARBALL_PATH]$TARBALL_PATH" diff --git a/build/azure-pipelines/linux/product-build-alpine.yml b/build/azure-pipelines/linux/product-build-alpine.yml index 8376c079ce..ed0c35346c 100644 --- a/build/azure-pipelines/linux/product-build-alpine.yml +++ b/build/azure-pipelines/linux/product-build-alpine.yml @@ -117,19 +117,37 @@ steps: - script: | set -e - AZURE_DOCUMENTDB_MASTERKEY="$(builds-docdb-key-readwrite)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(vscode-storage-key)" \ - VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ - ./build/azure-pipelines/linux/alpine/publish.sh - displayName: Publish + REPO="$(pwd)" + ROOT="$REPO/.." + + PLATFORM_LINUX="linux-alpine" + + # Publish Remote Extension Host + LEGACY_SERVER_BUILD_NAME="vscode-reh-$PLATFORM_LINUX" + SERVER_BUILD_NAME="vscode-server-$PLATFORM_LINUX" + SERVER_TARBALL_FILENAME="vscode-server-$PLATFORM_LINUX.tar.gz" + SERVER_TARBALL_PATH="$ROOT/$SERVER_TARBALL_FILENAME" + + rm -rf $ROOT/vscode-server-*.tar.* + (cd $ROOT && mv $LEGACY_SERVER_BUILD_NAME $SERVER_BUILD_NAME && tar --owner=0 --group=0 -czf $SERVER_TARBALL_PATH $SERVER_BUILD_NAME) + + # 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) + displayName: Prepare for publish condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - publish: $(Agent.BuildDirectory)/vscode-server-linux-alpine.tar.gz - artifact: vscode-server-linux-alpine + artifact: vscode_server_linux_alpine_archive-unsigned 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 + artifact: vscode_web_linux_alpine_archive-unsigned displayName: Publish web server archive condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index cb06bf6a72..8181083d1f 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -245,27 +245,32 @@ steps: AZURE_STORAGE_ACCESS_KEY_2="$(vscode-storage-key)" \ VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ VSCODE_ARCH="$(VSCODE_ARCH)" \ - ./build/azure-pipelines/linux/publish.sh - displayName: Publish + ./build/azure-pipelines/linux/prepare-publish.sh + displayName: Prepare for Publish condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - publish: $(DEB_PATH) - artifact: vscode-linux-deb-$(VSCODE_ARCH) + artifact: vscode_client_linux_$(VSCODE_ARCH)_deb-package displayName: Publish deb package condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - publish: $(RPM_PATH) - artifact: vscode-linux-rpm-$(VSCODE_ARCH) + artifact: vscode_client_linux_$(VSCODE_ARCH)_rpm-package displayName: Publish rpm package condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) + - publish: $(TARBALL_PATH) + artifact: vscode_client_linux_$(VSCODE_ARCH)_archive-unsigned + displayName: Publish client archive + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) + - publish: $(Agent.BuildDirectory)/vscode-server-linux-$(VSCODE_ARCH).tar.gz - artifact: vscode-server-linux-$(VSCODE_ARCH) + artifact: vscode_server_linux_$(VSCODE_ARCH)_archive-unsigned 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 + artifact: vscode_web_linux_$(VSCODE_ARCH)_archive-unsigned displayName: Publish web server archive condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) diff --git a/build/azure-pipelines/linux/snap-build-linux.yml b/build/azure-pipelines/linux/snap-build-linux.yml index f5e0288f0b..f7af900e1d 100644 --- a/build/azure-pipelines/linux/snap-build-linux.yml +++ b/build/azure-pipelines/linux/snap-build-linux.yml @@ -50,15 +50,11 @@ steps: esac (cd $SNAP_ROOT/code-* && sudo --preserve-env snapcraft prime $SNAPCRAFT_TARGET_ARGS && snap pack prime --compression=lzo --filename="$SNAP_PATH") - # Publish snap package - 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" + displayName: Prepare for publish - publish: $(SNAP_PATH) - artifact: vscode-linux-snap-$(VSCODE_ARCH) + artifact: vscode_client_linux_$(VSCODE_ARCH)_snap displayName: Publish snap package condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index fd698a0e7d..2c475b9ded 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -86,6 +86,8 @@ variables: value: ${{ eq(parameters.ENABLE_TERRAPIN, true) }} - name: VSCODE_QUALITY value: ${{ parameters.VSCODE_QUALITY }} + - name: VSCODE_RELEASE + value: ${{ parameters.VSCODE_RELEASE }} - 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 @@ -301,37 +303,30 @@ stages: steps: - template: darwin/product-build-darwin-sign.yml - - ${{ if and(eq(variables['VSCODE_PUBLISH'], true), eq(parameters.VSCODE_COMPILE_ONLY, false)) }}: - - stage: Mooncake + - ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), ne(variables['VSCODE_PUBLISH'], 'false')) }}: + - stage: Publish 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 - condition: succeededOrFailed() + - Compile pool: vmImage: "Ubuntu-18.04" + variables: + - name: BUILDS_API_URL + value: $(System.CollectionUri)$(System.TeamProject)/_apis/build/builds/$(Build.BuildId)/ jobs: - - job: SyncMooncake - displayName: Sync Mooncake + - job: PublishBuild + timeoutInMinutes: 180 + displayName: Publish Build steps: - - template: sync-mooncake.yml + - template: product-publish.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 + - ${{ if or(eq(parameters.VSCODE_RELEASE, true), and(in(parameters.VSCODE_QUALITY, 'insider', 'exploration'), eq(variables['VSCODE_SCHEDULEDBUILD'], true))) }}: + - stage: Release + dependsOn: + - Publish + pool: + vmImage: "Ubuntu-18.04" + jobs: + - job: ReleaseBuild + displayName: Release Build + steps: + - template: product-release.yml diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index 52c7758cfd..18c17639b8 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -118,14 +118,6 @@ steps: displayName: Publish Webview condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - - script: | - set -e - VERSION=`node -p "require(\"./package.json\").version"` - AZURE_DOCUMENTDB_MASTERKEY="$(builds-docdb-key-readwrite)" \ - node build/azure-pipelines/common/createBuild.js $VERSION - displayName: Create build - condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - # we gotta tarball everything in order to preserve file permissions - script: | set -e diff --git a/build/azure-pipelines/product-publish.ps1 b/build/azure-pipelines/product-publish.ps1 new file mode 100644 index 0000000000..339002ab0c --- /dev/null +++ b/build/azure-pipelines/product-publish.ps1 @@ -0,0 +1,114 @@ +. build/azure-pipelines/win32/exec.ps1 +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' +$ARTIFACT_PROCESSED_WILDCARD_PATH = "$env:PIPELINE_WORKSPACE/artifacts_processed_*/artifacts_processed_*" +$ARTIFACT_PROCESSED_FILE_PATH = "$env:PIPELINE_WORKSPACE/artifacts_processed_$env:SYSTEM_STAGEATTEMPT/artifacts_processed_$env:SYSTEM_STAGEATTEMPT.txt" + +function Get-PipelineArtifact { + param($Name = '*') + try { + $res = Invoke-RestMethod "$($env:BUILDS_API_URL)artifacts?api-version=6.0" -Headers @{ + Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" + } -MaximumRetryCount 5 -RetryIntervalSec 1 + + if (!$res) { + return + } + + $res.value | Where-Object { $_.name -Like $Name } + } catch { + Write-Warning $_ + } +} + +# This set will keep track of which artifacts have already been processed +$set = [System.Collections.Generic.HashSet[string]]::new() + +if (Test-Path $ARTIFACT_PROCESSED_WILDCARD_PATH) { + # Grab the latest artifact_processed text file and load all assets already processed from that. + # This means that the latest artifact_processed_*.txt file has all of the contents of the previous ones. + # Note: The kusto-like syntax only works in PS7+ and only in scripts, not at the REPL. + Get-ChildItem $ARTIFACT_PROCESSED_WILDCARD_PATH + | Sort-Object + | Select-Object -Last 1 + | Get-Content + | ForEach-Object { + $set.Add($_) | Out-Null + Write-Host "Already processed artifact: $_" + } +} + +# Create the artifact file that will be used for this run +New-Item -Path $ARTIFACT_PROCESSED_FILE_PATH -Force | Out-Null + +# Determine which stages we need to watch +$stages = @( + if ($env:VSCODE_BUILD_STAGE_WINDOWS -eq 'True') { 'Windows' } + if ($env:VSCODE_BUILD_STAGE_LINUX -eq 'True') { 'Linux' } + if ($env:VSCODE_BUILD_STAGE_MACOS -eq 'True') { 'macOS' } +) + +do { + Start-Sleep -Seconds 10 + + $artifacts = Get-PipelineArtifact -Name 'vscode_*' + if (!$artifacts) { + continue + } + + $artifacts | ForEach-Object { + $artifactName = $_.name + if($set.Add($artifactName)) { + Write-Host "Processing artifact: '$artifactName. Downloading from: $($_.resource.downloadUrl)" + + try { + Invoke-RestMethod $_.resource.downloadUrl -OutFile "$env:AGENT_TEMPDIRECTORY/$artifactName.zip" -Headers @{ + Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" + } -MaximumRetryCount 5 -RetryIntervalSec 1 | Out-Null + + Expand-Archive -Path "$env:AGENT_TEMPDIRECTORY/$artifactName.zip" -DestinationPath $env:AGENT_TEMPDIRECTORY | Out-Null + } catch { + Write-Warning $_ + $set.Remove($artifactName) | Out-Null + continue + } + + $null,$product,$os,$arch,$type = $artifactName -split '_' + $asset = Get-ChildItem -rec "$env:AGENT_TEMPDIRECTORY/$artifactName" + Write-Host "Processing artifact with the following values:" + # turning in into an object just to log nicely + @{ + product = $product + os = $os + arch = $arch + type = $type + asset = $asset.Name + } | Format-Table + + exec { node build/azure-pipelines/common/createAsset.js $product $os $arch $type $asset.Name $asset.FullName } + $artifactName >> $ARTIFACT_PROCESSED_FILE_PATH + } + } + + # Get the timeline and see if it says the other stage completed + try { + $timeline = Invoke-RestMethod "$($env:BUILDS_API_URL)timeline?api-version=6.0" -Headers @{ + Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" + } -MaximumRetryCount 5 -RetryIntervalSec 1 + } catch { + Write-Warning $_ + continue + } + + foreach ($stage in $stages) { + $otherStageFinished = $timeline.records | Where-Object { $_.name -eq $stage -and $_.type -eq 'stage' -and $_.state -eq 'completed' } + if (!$otherStageFinished) { + break + } + } + + $artifacts = Get-PipelineArtifact -Name 'vscode_*' + $artifactsStillToProcess = $artifacts.Count -ne $set.Count +} while (!$otherStageFinished -or $artifactsStillToProcess) + +Write-Host "Processed $($set.Count) artifacts." diff --git a/build/azure-pipelines/product-publish.yml b/build/azure-pipelines/product-publish.yml new file mode 100644 index 0000000000..de8cb216b8 --- /dev/null +++ b/build/azure-pipelines/product-publish.yml @@ -0,0 +1,89 @@ +steps: + - task: NodeTool@0 + inputs: + versionSpec: "12.x" + + - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.x" + + - task: AzureKeyVault@1 + displayName: "Azure Key Vault: Get Secrets" + inputs: + azureSubscription: "vscode-builds-subscription" + KeyVaultName: vscode + + - pwsh: | + . build/azure-pipelines/win32/exec.ps1 + cd build + exec { yarn } + displayName: Install dependencies + + - download: current + patterns: '**/artifacts_processed_*.txt' + displayName: Download all artifacts_processed text files + + - pwsh: | + . build/azure-pipelines/win32/exec.ps1 + + if (Test-Path "$(Pipeline.Workspace)/artifacts_processed_*/artifacts_processed_*.txt") { + Write-Host "Artifacts already processed so a build must have already been created." + return + } + + $env:AZURE_DOCUMENTDB_MASTERKEY = "$(builds-docdb-key-readwrite)" + $VERSION = node -p "require('./package.json').version" + Write-Host "Creating build with version: $VERSION" + exec { node build/azure-pipelines/common/createBuild.js $VERSION } + displayName: Create build if it hasn't been created before + + - pwsh: | + $env:VSCODE_MIXIN_PASSWORD = "$(github-distro-mixin-password)" + $env:AZURE_DOCUMENTDB_MASTERKEY = "$(builds-docdb-key-readwrite)" + $env:AZURE_STORAGE_ACCESS_KEY = "$(ticino-storage-key)" + $env:AZURE_STORAGE_ACCESS_KEY_2 = "$(vscode-storage-key)" + $env:MOONCAKE_STORAGE_ACCESS_KEY = "$(vscode-mooncake-storage-key)" + build/azure-pipelines/product-publish.ps1 + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + displayName: Process artifacts + + - publish: $(Pipeline.Workspace)/artifacts_processed_$(System.StageAttempt)/artifacts_processed_$(System.StageAttempt).txt + artifact: artifacts_processed_$(System.StageAttempt) + displayName: Publish what artifacts were published for this stage attempt + + - pwsh: | + $ErrorActionPreference = 'Stop' + + # Determine which stages we need to watch + $stages = @( + if ($env:VSCODE_BUILD_STAGE_WINDOWS -eq 'True') { 'Windows' } + if ($env:VSCODE_BUILD_STAGE_LINUX -eq 'True') { 'Linux' } + if ($env:VSCODE_BUILD_STAGE_MACOS -eq 'True') { 'macOS' } + ) + Write-Host "Stages to check: $stages" + + # Get the timeline and see if it says the other stage completed + $timeline = Invoke-RestMethod "$($env:BUILDS_API_URL)timeline?api-version=6.0" -Headers @{ + Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" + } -MaximumRetryCount 5 -RetryIntervalSec 1 + + $failedStages = @() + foreach ($stage in $stages) { + $didStageFail = $timeline.records | Where-Object { + $_.name -eq $stage -and $_.type -eq 'stage' -and $_.result -ne 'succeeded' -and $_.result -ne 'succeededWithIssues' + } + + if($didStageFail) { + $failedStages += $stage + } else { + Write-Host "'$stage' did not fail." + } + } + + if ($failedStages.Length) { + throw "Failed stages: $($failedStages -join ', '). This stage will now fail so that it is easier to retry failed jobs." + } + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + displayName: Determine if stage should succeed diff --git a/build/azure-pipelines/release.yml b/build/azure-pipelines/product-release.yml similarity index 100% rename from build/azure-pipelines/release.yml rename to build/azure-pipelines/product-release.yml diff --git a/build/azure-pipelines/publish-types/update-types.js b/build/azure-pipelines/publish-types/update-types.js index 6fc1d6b990..2da8ae32e9 100644 --- a/build/azure-pipelines/publish-types/update-types.js +++ b/build/azure-pipelines/publish-types/update-types.js @@ -63,8 +63,8 @@ function getNewFileHeader(tag) { ``, `/*---------------------------------------------------------------------------------------------`, ` * Copyright (c) Microsoft Corporation. All rights reserved.`, - ` * Licensed under the MIT License.`, - ` * See https://github.com/Microsoft/azuredatastudio/blob/main/LICENSE.txt for license information.`, + ` * Licensed under the Source EULA.`, + ` * See https://github.com/microsoft/azuredatastudio/blob/main/LICENSE.txt for license information.`, ` *--------------------------------------------------------------------------------------------*/`, ``, `/**`, diff --git a/build/azure-pipelines/publish-types/update-types.ts b/build/azure-pipelines/publish-types/update-types.ts index fd1ab41301..c3ed3324a7 100644 --- a/build/azure-pipelines/publish-types/update-types.ts +++ b/build/azure-pipelines/publish-types/update-types.ts @@ -75,8 +75,8 @@ function getNewFileHeader(tag: string) { ``, `/*---------------------------------------------------------------------------------------------`, ` * Copyright (c) Microsoft Corporation. All rights reserved.`, - ` * Licensed under the MIT License.`, - ` * See https://github.com/Microsoft/azuredatastudio/blob/main/LICENSE.txt for license information.`, + ` * Licensed under the Source EULA.`, + ` * See https://github.com/microsoft/azuredatastudio/blob/main/LICENSE.txt for license information.`, ` *--------------------------------------------------------------------------------------------*/`, ``, `/**`, diff --git a/build/azure-pipelines/sync-mooncake.yml b/build/azure-pipelines/sync-mooncake.yml deleted file mode 100644 index 6e379754f2..0000000000 --- a/build/azure-pipelines/sync-mooncake.yml +++ /dev/null @@ -1,24 +0,0 @@ -steps: - - task: NodeTool@0 - inputs: - versionSpec: "14.x" - - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.x" - - - task: AzureKeyVault@1 - displayName: "Azure Key Vault: Get Secrets" - inputs: - azureSubscription: "vscode-builds-subscription" - KeyVaultName: vscode - - - script: | - set -e - - (cd build ; yarn) - - AZURE_DOCUMENTDB_MASTERKEY="$(builds-docdb-key-readwrite)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(vscode-storage-key)" \ - MOONCAKE_STORAGE_ACCESS_KEY="$(vscode-mooncake-storage-key)" \ - node build/azure-pipelines/common/sync-mooncake.js "$VSCODE_QUALITY" diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index 772fe1c05a..45dedea1b4 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -119,13 +119,19 @@ steps: - script: | set -e - AZURE_DOCUMENTDB_MASTERKEY="$(builds-docdb-key-readwrite)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(vscode-storage-key)" \ - VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ - ./build/azure-pipelines/web/publish.sh - displayName: Publish + REPO="$(pwd)" + ROOT="$REPO/.." + + WEB_BUILD_NAME="vscode-web" + WEB_TARBALL_FILENAME="vscode-web.tar.gz" + WEB_TARBALL_PATH="$ROOT/$WEB_TARBALL_FILENAME" + + rm -rf $ROOT/vscode-web.tar.* + + cd $ROOT && tar --owner=0 --group=0 -czf $WEB_TARBALL_PATH $WEB_BUILD_NAME + displayName: Prepare for publish - publish: $(Agent.BuildDirectory)/vscode-web.tar.gz - artifact: vscode-web-standalone + artifact: vscode_web_linux_standalone_archive-unsigned displayName: Publish web archive condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) diff --git a/build/azure-pipelines/web/publish.sh b/build/azure-pipelines/web/publish.sh deleted file mode 100755 index 827edc2661..0000000000 --- a/build/azure-pipelines/web/publish.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -set -e -REPO="$(pwd)" -ROOT="$REPO/.." - -# Publish Web Client -WEB_BUILD_NAME="vscode-web" -WEB_TARBALL_FILENAME="vscode-web.tar.gz" -WEB_TARBALL_PATH="$ROOT/$WEB_TARBALL_FILENAME" - -rm -rf $ROOT/vscode-web.tar.* - -(cd $ROOT && tar --owner=0 --group=0 -czf $WEB_TARBALL_PATH $WEB_BUILD_NAME) - -node build/azure-pipelines/common/createAsset.js web-standalone archive-unsigned "$WEB_TARBALL_FILENAME" "$WEB_TARBALL_PATH" diff --git a/build/azure-pipelines/win32/publish.ps1 b/build/azure-pipelines/win32/prepare-publish.ps1 similarity index 51% rename from build/azure-pipelines/win32/publish.ps1 rename to build/azure-pipelines/win32/prepare-publish.ps1 index a225f9d5fd..f80e1ca0ce 100644 --- a/build/azure-pipelines/win32/publish.ps1 +++ b/build/azure-pipelines/win32/prepare-publish.ps1 @@ -13,24 +13,31 @@ $Zip = "$Repo\.build\win32-$Arch\archive\VSCode-win32-$Arch.zip" $LegacyServer = "$Root\vscode-reh-win32-$Arch" $Server = "$Root\vscode-server-win32-$Arch" $ServerZip = "$Repo\.build\vscode-server-win32-$Arch.zip" +$LegacyWeb = "$Root\vscode-reh-web-win32-$Arch" +$Web = "$Root\vscode-server-win32-$Arch-web" +$WebZip = "$Repo\.build\vscode-server-win32-$Arch-web.zip" $Build = "$Root\VSCode-win32-$Arch" # Create server archive if ("$Arch" -ne "arm64") { exec { xcopy $LegacyServer $Server /H /E /I } exec { .\node_modules\7zip\7zip-lite\7z.exe a -tzip $ServerZip $Server -r } + exec { xcopy $LegacyWeb $Web /H /E /I } + exec { .\node_modules\7zip\7zip-lite\7z.exe a -tzip $WebZip $Web -r } } # get version $PackageJson = Get-Content -Raw -Path "$Build\resources\app\package.json" | ConvertFrom-Json $Version = $PackageJson.version -$AssetPlatform = if ("$Arch" -eq "ia32") { "win32" } else { "win32-$Arch" } +$ARCHIVE_NAME = "VSCode-win32-$Arch-$Version.zip" +$SYSTEM_SETUP_NAME = "VSCodeSetup-$Arch-$Version.exe" +$USER_SETUP_NAME = "VSCodeUserSetup-$Arch-$Version.exe" -exec { node build/azure-pipelines/common/createAsset.js "$AssetPlatform-archive" archive "VSCode-win32-$Arch-$Version.zip" $Zip } -exec { node build/azure-pipelines/common/createAsset.js "$AssetPlatform" setup "VSCodeSetup-$Arch-$Version.exe" $SystemExe } -exec { node build/azure-pipelines/common/createAsset.js "$AssetPlatform-user" setup "VSCodeUserSetup-$Arch-$Version.exe" $UserExe } - -if ("$Arch" -ne "arm64") { - exec { node build/azure-pipelines/common/createAsset.js "server-$AssetPlatform" archive "vscode-server-win32-$Arch.zip" $ServerZip } -} +# Set variables for upload +Move-Item $Zip "$Repo\.build\win32-$Arch\archive\$ARCHIVE_NAME" +Write-Host "##vso[task.setvariable variable=ARCHIVE_NAME]$ARCHIVE_NAME" +Move-Item $SystemExe "$Repo\.build\win32-$Arch\system-setup\$SYSTEM_SETUP_NAME" +Write-Host "##vso[task.setvariable variable=SYSTEM_SETUP_NAME]$SYSTEM_SETUP_NAME" +Move-Item $UserExe "$Repo\.build\win32-$Arch\user-setup\$USER_SETUP_NAME" +Write-Host "##vso[task.setvariable variable=USER_SETUP_NAME]$USER_SETUP_NAME" diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 2dcaf8b2e0..1f8514ae7e 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -295,31 +295,31 @@ steps: $env:AZURE_STORAGE_ACCESS_KEY_2 = "$(vscode-storage-key)" $env:AZURE_DOCUMENTDB_MASTERKEY = "$(builds-docdb-key-readwrite)" $env:VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" - .\build\azure-pipelines\win32\publish.ps1 + .\build\azure-pipelines\win32\prepare-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) + - publish: $(System.DefaultWorkingDirectory)\.build\win32-$(VSCODE_ARCH)\archive\$(ARCHIVE_NAME) + artifact: vscode_client_win32_$(VSCODE_ARCH)_archive 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 + - publish: $(System.DefaultWorkingDirectory)\.build\win32-$(VSCODE_ARCH)\system-setup\$(SYSTEM_SETUP_NAME) + artifact: vscode_client_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 + - publish: $(System.DefaultWorkingDirectory)\.build\win32-$(VSCODE_ARCH)\user-setup\$(USER_SETUP_NAME) + artifact: vscode_client_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) + artifact: vscode_server_win32_$(VSCODE_ARCH)_archive 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 + artifact: vscode_web_win32_$(VSCODE_ARCH)_archive displayName: Publish web server archive condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) diff --git a/build/darwin/create-universal-app.js b/build/darwin/create-universal-app.js index 5e4ba3e611..44600c28e1 100644 --- a/build/darwin/create-universal-app.js +++ b/build/darwin/create-universal-app.js @@ -23,7 +23,7 @@ async function main() { 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 (0, vscode_universal_1.makeUniversalApp)({ + await vscode_universal_1.makeUniversalApp({ x64AppPath, arm64AppPath, x64AsarPath, diff --git a/build/filters.js b/build/filters.js index 3dc3db6a46..665d4ed372 100644 --- a/build/filters.js +++ b/build/filters.js @@ -51,7 +51,7 @@ module.exports.indentationFilter = [ '!test/monaco/out/**', '!test/smoke/out/**', '!extensions/typescript-language-features/test-workspace/**', - '!extensions/notebook-markdown-extensions/notebook-out/**', + '!extensions/markdown-math/notebook-out/**', '!extensions/vscode-api-tests/testWorkspace/**', '!extensions/vscode-api-tests/testWorkspace2/**', '!extensions/vscode-custom-editor-tests/test-workspace/**', @@ -89,7 +89,7 @@ module.exports.indentationFilter = [ '!**/*.dockerfile', '!extensions/markdown-language-features/media/*.js', '!extensions/markdown-language-features/notebook-out/*.js', - '!extensions/notebook-markdown-extensions/notebook-out/*.js', + '!extensions/markdown-math/notebook-out/*.js', '!extensions/simple-browser/media/*.js', ]; @@ -119,7 +119,7 @@ module.exports.copyrightFilter = [ '!resources/completions/**', '!extensions/configuration-editing/build/inline-allOf.ts', '!extensions/markdown-language-features/media/highlight.css', - '!extensions/notebook-markdown-extensions/notebook-out/**', + '!extensions/markdown-math/notebook-out/**', '!extensions/html-language-features/server/src/modes/typescript/*', '!extensions/*/server/bin/*', '!src/vs/editor/test/node/classification/typescript-test.ts', diff --git a/build/gulpfile.editor.js b/build/gulpfile.editor.js index 0e56c448f0..1706287c84 100644 --- a/build/gulpfile.editor.js +++ b/build/gulpfile.editor.js @@ -14,7 +14,7 @@ const i18n = require('./lib/i18n'); const standalone = require('./lib/standalone'); const cp = require('child_process'); const compilation = require('./lib/compilation'); -const monacoapi = require('./monaco/api'); +const monacoapi = require('./lib/monaco-api'); const fs = require('fs'); let root = path.dirname(__dirname); @@ -49,7 +49,7 @@ let BUNDLED_FILE_HEADER = [ ' * Copyright (c) Microsoft Corporation. All rights reserved.', ' * Version: ' + headerVersion, ' * Released under the Source EULA', - ' * https://github.com/Microsoft/vscode/blob/master/LICENSE.txt', + ' * https://github.com/microsoft/vscode/blob/main/LICENSE.txt', ' *-----------------------------------------------------------*/', '' ].join('\n'); @@ -279,7 +279,7 @@ const finalEditorResourcesTask = task.define('final-editor-resources', () => { // version.txt gulp.src('build/monaco/version.txt') .pipe(es.through(function (data) { - data.contents = Buffer.from(`monaco-editor-core: https://github.com/Microsoft/vscode/tree/${sha1}`); + data.contents = Buffer.from(`monaco-editor-core: https://github.com/microsoft/vscode/tree/${sha1}`); this.emit('data', data); })) .pipe(gulp.dest('out-monaco-editor-core')), diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index c03b0c4f0f..617cc47a96 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -8,7 +8,6 @@ require('events').EventEmitter.defaultMaxListeners = 100; const gulp = require('gulp'); const path = require('path'); -const child_process = require('child_process'); const nodeUtil = require('util'); const es = require('event-stream'); const filter = require('gulp-filter'); @@ -20,8 +19,6 @@ const glob = require('glob'); const root = path.dirname(__dirname); const commit = util.getVersion(root); const plumber = require('gulp-plumber'); -const fancyLog = require('fancy-log'); -const ansiColors = require('ansi-colors'); const ext = require('./lib/extensions'); const extensionsPath = path.join(path.dirname(__dirname), 'extensions'); @@ -59,6 +56,7 @@ const compilations = glob.sync('**/tsconfig.json', { // 'json-language-features/server/tsconfig.json', // 'markdown-language-features/preview-src/tsconfig.json', // 'markdown-language-features/tsconfig.json', +// 'markdown-math/tsconfig.json', // 'merge-conflict/tsconfig.json', // 'microsoft-authentication/tsconfig.json', // 'npm/tsconfig.json', @@ -207,45 +205,17 @@ gulp.task(compileExtensionsBuildLegacyTask); //#region Extension media -// Additional projects to webpack. These typically build code for webviews -const webpackMediaConfigFiles = [ - 'markdown-language-features/webpack.config.js', - 'simple-browser/webpack.config.js', -]; - -// Additional projects to run esbuild on. These typically build code for webviews -const esbuildMediaScripts = [ - 'markdown-language-features/esbuild.js', - 'notebook-markdown-extensions/esbuild.js', -]; - -const compileExtensionMediaTask = task.define('compile-extension-media', () => buildExtensionMedia(false)); +const compileExtensionMediaTask = task.define('compile-extension-media', () => ext.buildExtensionMedia(false)); gulp.task(compileExtensionMediaTask); exports.compileExtensionMediaTask = compileExtensionMediaTask; -const watchExtensionMedia = task.define('watch-extension-media', () => buildExtensionMedia(true)); +const watchExtensionMedia = task.define('watch-extension-media', () => ext.buildExtensionMedia(true)); gulp.task(watchExtensionMedia); exports.watchExtensionMedia = watchExtensionMedia; -const compileExtensionMediaBuildTask = task.define('compile-extension-media-build', () => buildExtensionMedia(false, '.build/extensions')); +const compileExtensionMediaBuildTask = task.define('compile-extension-media-build', () => ext.buildExtensionMedia(false, '.build/extensions')); gulp.task(compileExtensionMediaBuildTask); -async function buildExtensionMedia(isWatch, outputRoot) { - const webpackConfigLocations = webpackMediaConfigFiles.map(p => { - return { - configPath: path.join(extensionsPath, p), - outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined - }; - }); - return Promise.all([ - webpackExtensions('webpacking extension media', isWatch, webpackConfigLocations), - esbuildExtensions('esbuilding extension media', isWatch, esbuildMediaScripts.map(p => ({ - script: path.join(extensionsPath, p), - outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined - }))), - ]); -} - //#endregion //#region Azure Pipelines @@ -320,121 +290,5 @@ async function buildWebExtensions(isWatch) { path.join(extensionsPath, '**', 'extension-browser.webpack.config.js'), { ignore: ['**/node_modules'] } ); - return webpackExtensions('packaging web extension', isWatch, webpackConfigLocations.map(configPath => ({ configPath }))); -} - -/** - * @param {string} taskName - * @param {boolean} isWatch - * @param {{ configPath: string, outputRoot?: boolean}} webpackConfigLocations - */ -async function webpackExtensions(taskName, isWatch, webpackConfigLocations) { - const webpack = require('webpack'); - - const webpackConfigs = []; - - for (const { configPath, outputRoot } of webpackConfigLocations) { - const configOrFnOrArray = require(configPath); - function addConfig(configOrFn) { - let config; - if (typeof configOrFn === 'function') { - config = configOrFn({}, {}); - webpackConfigs.push(config); - } else { - config = configOrFn; - } - - if (outputRoot) { - config.output.path = path.join(outputRoot, path.relative(path.dirname(configPath), config.output.path)); - } - - webpackConfigs.push(configOrFn); - } - addConfig(configOrFnOrArray); - } - function reporter(fullStats) { - if (Array.isArray(fullStats.children)) { - for (const stats of fullStats.children) { - const outputPath = stats.outputPath; - if (outputPath) { - const relativePath = path.relative(extensionsPath, outputPath).replace(/\\/g, '/'); - const match = relativePath.match(/[^\/]+(\/server|\/client)?/); - fancyLog(`Finished ${ansiColors.green(taskName)} ${ansiColors.cyan(match[0])} with ${stats.errors.length} errors.`); - } - if (Array.isArray(stats.errors)) { - stats.errors.forEach(error => { - fancyLog.error(error); - }); - } - if (Array.isArray(stats.warnings)) { - stats.warnings.forEach(warning => { - fancyLog.warn(warning); - }); - } - } - } - } - return new Promise((resolve, reject) => { - if (isWatch) { - webpack(webpackConfigs).watch({}, (err, stats) => { - if (err) { - reject(); - } else { - reporter(stats.toJson()); - } - }); - } else { - webpack(webpackConfigs).run((err, stats) => { - if (err) { - fancyLog.error(err); - reject(); - } else { - reporter(stats.toJson()); - resolve(); - } - }); - } - }); -} - -/** - * @param {string} taskName - * @param {boolean} isWatch - * @param {{ script: string, outputRoot?: string }}} scripts - */ -async function esbuildExtensions(taskName, isWatch, scripts) { - function reporter(/** @type {string} */ stdError, /** @type {string} */script) { - const matches = (stdError || '').match(/\> (.+): error: (.+)?/g); - fancyLog(`Finished ${ansiColors.green(taskName)} ${script} with ${matches ? matches.length : 0} errors.`); - for (const match of matches || []) { - fancyLog.error(match); - } - } - - const tasks = scripts.map(({ script, outputRoot }) => { - return new Promise((resolve, reject) => { - const args = [script]; - if (isWatch) { - args.push('--watch'); - } - if (outputRoot) { - args.push('--outputRoot', outputRoot); - } - const proc = child_process.execFile(process.argv[0], args, {}, (error, _stdout, stderr) => { - if (error) { - return reject(error); - } - reporter(stderr, script); - if (stderr) { - return reject(); - } - return resolve(); - }); - - proc.stdout.on('data', (data) => { - fancyLog(`${ansiColors.green(taskName)}: ${data.toString('utf8')}`); - }); - }); - }); - return Promise.all(tasks); + return ext.webpackExtensions('packaging web extension', isWatch, webpackConfigLocations.map(configPath => ({ configPath }))); } diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 8c405c4520..71eea44795 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -283,7 +283,14 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op .pipe(jsFilter) .pipe(util.rewriteSourceMappingURL(sourceMappingURLBase)) .pipe(jsFilter.restore) - .pipe(createAsar(path.join(process.cwd(), 'node_modules'), ['**/*.node', '**/vscode-ripgrep/bin/*', '**/node-pty/build/Release/*', '**/*.wasm'], 'node_modules.asar')); + .pipe(createAsar(path.join(process.cwd(), 'node_modules'), [ + '**/*.node', + '**/vscode-ripgrep/bin/*', + '**/node-pty/build/Release/*', + '**/node-pty/lib/worker/conoutSocketWorker.js', + '**/node-pty/lib/shared/conout.js', + '**/*.wasm' + ], 'node_modules.asar')); let all = es.merge( packageJsonStream, @@ -439,8 +446,6 @@ BUILD_TARGETS.forEach(buildTarget => { } }); -// Transifex Localizations - const innoSetupConfig = { 'zh-cn': { codePage: 'CP936', defaultInfo: { name: 'Simplified Chinese', id: '$0804', } }, 'zh-tw': { codePage: 'CP950', defaultInfo: { name: 'Traditional Chinese', id: '$0404' } }, @@ -456,6 +461,8 @@ const innoSetupConfig = { 'tr': { codePage: 'CP1254' } }; +// Transifex Localizations + const apiHostname = process.env.TRANSIFEX_API_URL; const apiName = process.env.TRANSIFEX_API_NAME; const apiToken = process.env.TRANSIFEX_API_TOKEN; @@ -491,7 +498,7 @@ const vscodeTranslationsExport = task.define( function () { const pathToMetadata = './out-vscode/nls.metadata.json'; const pathToExtensions = '.build/extensions/*'; - const pathToSetup = 'build/win32/**/{Default.isl,messages.en.isl}'; + const pathToSetup = 'build/win32/i18n/messages.en.isl'; return es.merge( gulp.src(pathToMetadata).pipe(i18n.createXlfFilesForCoreBundle()), diff --git a/build/lib/builtInExtensions.js b/build/lib/builtInExtensions.js index 7495f0595a..a01b233d0f 100644 --- a/build/lib/builtInExtensions.js +++ b/build/lib/builtInExtensions.js @@ -18,8 +18,8 @@ const ansiColors = require("ansi-colors"); const mkdirp = require('mkdirp'); 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 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) { diff --git a/build/lib/builtInExtensions.ts b/build/lib/builtInExtensions.ts index 4c9fc5d22f..eee9f99158 100644 --- a/build/lib/builtInExtensions.ts +++ b/build/lib/builtInExtensions.ts @@ -36,8 +36,8 @@ export interface IExtensionDefinition { 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 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']; diff --git a/build/lib/builtInExtensionsCG.js b/build/lib/builtInExtensionsCG.js index 64b9064c8e..a08b72b3ec 100644 --- a/build/lib/builtInExtensionsCG.js +++ b/build/lib/builtInExtensionsCG.js @@ -1,7 +1,7 @@ "use strict"; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); const got_1 = require("got"); @@ -12,8 +12,8 @@ const ansiColors = require("ansi-colors"); const root = path.dirname(path.dirname(__dirname)); const rootCG = path.join(root, 'extensionsCG'); const productjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); -const builtInExtensions = productjson.builtInExtensions; -const webBuiltInExtensions = productjson.webBuiltInExtensions; +const builtInExtensions = productjson.builtInExtensions || []; +const webBuiltInExtensions = productjson.webBuiltInExtensions || []; const token = process.env['VSCODE_MIXIN_PASSWORD'] || process.env['GITHUB_TOKEN'] || undefined; const contentBasePath = 'raw.githubusercontent.com'; const contentFileNames = ['package.json', 'package-lock.json', 'yarn.lock']; @@ -25,7 +25,7 @@ async function downloadExtensionDetails(extension) { const promises = []; for (const fileName of contentFileNames) { promises.push(new Promise(resolve => { - (0, got_1.default)(`${repositoryContentBaseUrl}/${fileName}`) + got_1.default(`${repositoryContentBaseUrl}/${fileName}`) .then(response => { resolve({ fileName, body: response.rawBody }); }) diff --git a/build/lib/builtInExtensionsCG.ts b/build/lib/builtInExtensionsCG.ts index 45785529b6..21c970e5f7 100644 --- a/build/lib/builtInExtensionsCG.ts +++ b/build/lib/builtInExtensionsCG.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import got from 'got'; @@ -13,8 +13,8 @@ import { IExtensionDefinition } from './builtInExtensions'; const root = path.dirname(path.dirname(__dirname)); const rootCG = path.join(root, 'extensionsCG'); const productjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); -const builtInExtensions = productjson.builtInExtensions; -const webBuiltInExtensions = productjson.webBuiltInExtensions; +const builtInExtensions = productjson.builtInExtensions || []; +const webBuiltInExtensions = productjson.webBuiltInExtensions || []; const token = process.env['VSCODE_MIXIN_PASSWORD'] || process.env['GITHUB_TOKEN'] || undefined; const contentBasePath = 'raw.githubusercontent.com'; diff --git a/build/lib/compilation.js b/build/lib/compilation.js index b545199e78..22a8ee8be1 100644 --- a/build/lib/compilation.js +++ b/build/lib/compilation.js @@ -9,7 +9,7 @@ const es = require("event-stream"); const fs = require("fs"); const gulp = require("gulp"); const path = require("path"); -const monacodts = require("../monaco/api"); +const monacodts = require("./monaco-api"); const nls = require("./nls"); const reporter_1 = require("./reporter"); const util = require("./util"); @@ -17,7 +17,7 @@ const fancyLog = require("fancy-log"); const ansiColors = require("ansi-colors"); const os = require("os"); const watch = require('./watch'); -const reporter = (0, reporter_1.createReporter)(); +const reporter = reporter_1.createReporter(); function getTypeScriptCompilerOptions(src) { const rootDir = path.join(__dirname, `../../${src}`); let options = {}; diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index 282dae530e..610a7999f5 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -9,7 +9,7 @@ import * as es from 'event-stream'; import * as fs from 'fs'; import * as gulp from 'gulp'; import * as path from 'path'; -import * as monacodts from '../monaco/api'; +import * as monacodts from './monaco-api'; import * as nls from './nls'; import { createReporter } from './reporter'; import * as util from './util'; diff --git a/build/lib/eslint/code-import-patterns.js b/build/lib/eslint/code-import-patterns.js index 52adf71a64..5babda400c 100644 --- a/build/lib/eslint/code-import-patterns.js +++ b/build/lib/eslint/code-import-patterns.js @@ -21,7 +21,7 @@ module.exports = new class { const configs = context.options; for (const config of configs) { if (minimatch(context.getFilename(), config.target)) { - return (0, utils_1.createImportRuleListener)((node, value) => this._checkImport(context, config, node, value)); + return utils_1.createImportRuleListener((node, value) => this._checkImport(context, config, node, value)); } } return {}; @@ -29,7 +29,7 @@ module.exports = new class { _checkImport(context, config, node, path) { // resolve relative paths if (path[0] === '.') { - path = (0, path_1.join)(context.getFilename(), path); + path = path_1.join(context.getFilename(), path); } let restrictions; if (typeof config.restrictions === 'string') { diff --git a/build/lib/eslint/code-layering.js b/build/lib/eslint/code-layering.js index d8b70f5ac2..bac676755b 100644 --- a/build/lib/eslint/code-layering.js +++ b/build/lib/eslint/code-layering.js @@ -17,7 +17,7 @@ module.exports = new class { }; } create(context) { - const fileDirname = (0, path_1.dirname)(context.getFilename()); + const fileDirname = path_1.dirname(context.getFilename()); const parts = fileDirname.split(/\\|\//); const ruleArgs = context.options[0]; let config; @@ -39,11 +39,11 @@ module.exports = new class { // nothing return {}; } - return (0, utils_1.createImportRuleListener)((node, path) => { + return utils_1.createImportRuleListener((node, path) => { if (path[0] === '.') { - path = (0, path_1.join)((0, path_1.dirname)(context.getFilename()), path); + path = path_1.join(path_1.dirname(context.getFilename()), path); } - const parts = (0, path_1.dirname)(path).split(/\\|\//); + const parts = path_1.dirname(path).split(/\\|\//); for (let i = parts.length - 1; i >= 0; i--) { const part = parts[i]; if (config.allowed.has(part)) { diff --git a/build/lib/eslint/code-no-nls-in-standalone-editor.js b/build/lib/eslint/code-no-nls-in-standalone-editor.js index 5d508810d1..1f1eabfcba 100644 --- a/build/lib/eslint/code-no-nls-in-standalone-editor.js +++ b/build/lib/eslint/code-no-nls-in-standalone-editor.js @@ -20,10 +20,10 @@ module.exports = new class NoNlsInStandaloneEditorRule { || /vs(\/|\\)editor(\/|\\)editor.api/.test(fileName) || /vs(\/|\\)editor(\/|\\)editor.main/.test(fileName) || /vs(\/|\\)editor(\/|\\)editor.worker/.test(fileName)) { - return (0, utils_1.createImportRuleListener)((node, path) => { + return utils_1.createImportRuleListener((node, path) => { // resolve relative paths if (path[0] === '.') { - path = (0, path_1.join)(context.getFilename(), path); + path = path_1.join(context.getFilename(), path); } if (/vs(\/|\\)nls/.test(path)) { context.report({ diff --git a/build/lib/eslint/code-no-standalone-editor.js b/build/lib/eslint/code-no-standalone-editor.js index 5812f1a1cc..df97c4d7e0 100644 --- a/build/lib/eslint/code-no-standalone-editor.js +++ b/build/lib/eslint/code-no-standalone-editor.js @@ -21,10 +21,10 @@ module.exports = new class NoNlsInStandaloneEditorRule { // the vs/editor folder is allowed to use the standalone editor return {}; } - return (0, utils_1.createImportRuleListener)((node, path) => { + return utils_1.createImportRuleListener((node, path) => { // resolve relative paths if (path[0] === '.') { - path = (0, path_1.join)(context.getFilename(), path); + path = path_1.join(context.getFilename(), path); } if (/vs(\/|\\)editor(\/|\\)standalone(\/|\\)/.test(path) || /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone(\/|\\)/.test(path) diff --git a/build/lib/eslint/code-no-unused-expressions.js b/build/lib/eslint/code-no-unused-expressions.js index 21a29c94ea..bc6b7519a7 100644 --- a/build/lib/eslint/code-no-unused-expressions.js +++ b/build/lib/eslint/code-no-unused-expressions.js @@ -2,144 +2,124 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - // FORKED FROM https://github.com/eslint/eslint/blob/b23ad0d789a909baf8d7c41a35bc53df932eaf30/lib/rules/no-unused-expressions.js // and added support for `OptionalCallExpression`, see https://github.com/facebook/create-react-app/issues/8107 and https://github.com/eslint/eslint/issues/12642 - /** * @fileoverview Flag expressions in statement position that do not side effect * @author Michael Ficarra */ - 'use strict'; - +Object.defineProperty(exports, "__esModule", { value: true }); //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ - module.exports = { - meta: { - type: 'suggestion', - - docs: { - description: 'disallow unused expressions', - category: 'Best Practices', - recommended: false, - url: 'https://eslint.org/docs/rules/no-unused-expressions' - }, - - schema: [ - { - type: 'object', - properties: { - allowShortCircuit: { - type: 'boolean', - default: false - }, - allowTernary: { - type: 'boolean', - default: false - }, - allowTaggedTemplates: { - type: 'boolean', - default: false - } - }, - additionalProperties: false - } - ] - }, - - create(context) { - const config = context.options[0] || {}, - allowShortCircuit = config.allowShortCircuit || false, - allowTernary = config.allowTernary || false, - allowTaggedTemplates = config.allowTaggedTemplates || false; - - // eslint-disable-next-line jsdoc/require-description + meta: { + type: 'suggestion', + docs: { + description: 'disallow unused expressions', + category: 'Best Practices', + recommended: false, + url: 'https://eslint.org/docs/rules/no-unused-expressions' + }, + schema: [ + { + type: 'object', + properties: { + allowShortCircuit: { + type: 'boolean', + default: false + }, + allowTernary: { + type: 'boolean', + default: false + }, + allowTaggedTemplates: { + type: 'boolean', + default: false + } + }, + additionalProperties: false + } + ] + }, + create(context) { + const config = context.options[0] || {}, + allowShortCircuit = config.allowShortCircuit || false, + allowTernary = config.allowTernary || false, + allowTaggedTemplates = config.allowTaggedTemplates || false; + // eslint-disable-next-line jsdoc/require-description /** - * @param {ASTNode} node any node - * @returns {boolean} whether the given node structurally represents a directive + * @param node any node + * @returns whether the given node structurally represents a directive */ - function looksLikeDirective(node) { - return node.type === 'ExpressionStatement' && - node.expression.type === 'Literal' && typeof node.expression.value === 'string'; - } - - // eslint-disable-next-line jsdoc/require-description + function looksLikeDirective(node) { + return node.type === 'ExpressionStatement' && + node.expression.type === 'Literal' && typeof node.expression.value === 'string'; + } + // eslint-disable-next-line jsdoc/require-description /** - * @param {Function} predicate ([a] -> Boolean) the function used to make the determination - * @param {a[]} list the input list - * @returns {a[]} the leading sequence of members in the given list that pass the given predicate + * @param predicate ([a] -> Boolean) the function used to make the determination + * @param list the input list + * @returns the leading sequence of members in the given list that pass the given predicate */ - function takeWhile(predicate, list) { - for (let i = 0; i < list.length; ++i) { - if (!predicate(list[i])) { - return list.slice(0, i); - } - } - return list.slice(); - } - - // eslint-disable-next-line jsdoc/require-description + function takeWhile(predicate, list) { + for (let i = 0; i < list.length; ++i) { + if (!predicate(list[i])) { + return list.slice(0, i); + } + } + return list.slice(); + } + // eslint-disable-next-line jsdoc/require-description /** - * @param {ASTNode} node a Program or BlockStatement node - * @returns {ASTNode[]} the leading sequence of directive nodes in the given node's body + * @param node a Program or BlockStatement node + * @returns the leading sequence of directive nodes in the given node's body */ - function directives(node) { - return takeWhile(looksLikeDirective, node.body); - } - - // eslint-disable-next-line jsdoc/require-description + function directives(node) { + return takeWhile(looksLikeDirective, node.body); + } + // eslint-disable-next-line jsdoc/require-description /** - * @param {ASTNode} node any node - * @param {ASTNode[]} ancestors the given node's ancestors - * @returns {boolean} whether the given node is considered a directive in its current position + * @param node any node + * @param ancestors the given node's ancestors + * @returns whether the given node is considered a directive in its current position */ - function isDirective(node, ancestors) { - const parent = ancestors[ancestors.length - 1], - grandparent = ancestors[ancestors.length - 2]; - - return (parent.type === 'Program' || parent.type === 'BlockStatement' && - (/Function/u.test(grandparent.type))) && - directives(parent).indexOf(node) >= 0; - } - + function isDirective(node, ancestors) { + const parent = ancestors[ancestors.length - 1], grandparent = ancestors[ancestors.length - 2]; + return (parent.type === 'Program' || parent.type === 'BlockStatement' && + (/Function/u.test(grandparent.type))) && + directives(parent).indexOf(node) >= 0; + } /** * Determines whether or not a given node is a valid expression. Recurses on short circuit eval and ternary nodes if enabled by flags. - * @param {ASTNode} node any node - * @returns {boolean} whether the given node is a valid expression + * @param node any node + * @returns whether the given node is a valid expression */ - function isValidExpression(node) { - if (allowTernary) { - - // Recursive check for ternary and logical expressions - if (node.type === 'ConditionalExpression') { - return isValidExpression(node.consequent) && isValidExpression(node.alternate); - } - } - - if (allowShortCircuit) { - if (node.type === 'LogicalExpression') { - return isValidExpression(node.right); - } - } - - if (allowTaggedTemplates && node.type === 'TaggedTemplateExpression') { - return true; - } - - return /^(?:Assignment|OptionalCall|Call|New|Update|Yield|Await)Expression$/u.test(node.type) || - (node.type === 'UnaryExpression' && ['delete', 'void'].indexOf(node.operator) >= 0); - } - - return { - ExpressionStatement(node) { - if (!isValidExpression(node.expression) && !isDirective(node, context.getAncestors())) { - context.report({ node, message: 'Expected an assignment or function call and instead saw an expression.' }); - } - } - }; - - } + function isValidExpression(node) { + if (allowTernary) { + // Recursive check for ternary and logical expressions + if (node.type === 'ConditionalExpression') { + return isValidExpression(node.consequent) && isValidExpression(node.alternate); + } + } + if (allowShortCircuit) { + if (node.type === 'LogicalExpression') { + return isValidExpression(node.right); + } + } + if (allowTaggedTemplates && node.type === 'TaggedTemplateExpression') { + return true; + } + return /^(?:Assignment|OptionalCall|Call|New|Update|Yield|Await)Expression$/u.test(node.type) || + (node.type === 'UnaryExpression' && ['delete', 'void'].indexOf(node.operator) >= 0); + } + return { + ExpressionStatement(node) { + if (!isValidExpression(node.expression) && !isDirective(node, context.getAncestors())) { + context.report({ node: node, message: 'Expected an assignment or function call and instead saw an expression.' }); + } + } + }; + } }; diff --git a/build/lib/eslint/code-translation-remind.js b/build/lib/eslint/code-translation-remind.js index 4107285d76..01a39c82bb 100644 --- a/build/lib/eslint/code-translation-remind.js +++ b/build/lib/eslint/code-translation-remind.js @@ -15,7 +15,7 @@ module.exports = new (_a = class TranslationRemind { }; } create(context) { - return (0, utils_1.createImportRuleListener)((node, path) => this._checkImport(context, node, path)); + return utils_1.createImportRuleListener((node, path) => this._checkImport(context, node, path)); } _checkImport(context, node, path) { if (path !== TranslationRemind.NLS_MODULE) { @@ -31,7 +31,7 @@ module.exports = new (_a = class TranslationRemind { let resourceDefined = false; let json; try { - json = (0, fs_1.readFileSync)('./build/lib/i18n.resources.json', 'utf8'); + json = fs_1.readFileSync('./build/lib/i18n.resources.json', 'utf8'); } catch (e) { console.error('[translation-remind rule]: File with resources to pull from Transifex was not found. Aborting translation resource check for newly defined workbench part/service.'); diff --git a/build/lib/eslint/vscode-dts-vscode-in-comments.js b/build/lib/eslint/vscode-dts-vscode-in-comments.js new file mode 100644 index 0000000000..8f9a13fb01 --- /dev/null +++ b/build/lib/eslint/vscode-dts-vscode-in-comments.js @@ -0,0 +1,45 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +module.exports = new class ApiVsCodeInComments { + constructor() { + this.meta = { + messages: { + comment: `Don't use the term 'vs code' in comments` + } + }; + } + create(context) { + const sourceCode = context.getSourceCode(); + return { + ['Program']: (_node) => { + for (const comment of sourceCode.getAllComments()) { + if (comment.type !== 'Block') { + continue; + } + if (!comment.range) { + continue; + } + const startIndex = comment.range[0] + '/*'.length; + const re = /vs code/ig; + let match; + while ((match = re.exec(comment.value))) { + // Allow using 'VS Code' in quotes + if (comment.value[match.index - 1] === `'` && comment.value[match.index + match[0].length] === `'`) { + continue; + } + // Types for eslint seem incorrect + const start = sourceCode.getLocFromIndex(startIndex + match.index); + const end = sourceCode.getLocFromIndex(startIndex + match.index + match[0].length); + context.report({ + messageId: 'comment', + loc: { start, end } + }); + } + } + } + }; + } +}; diff --git a/build/lib/eslint/vscode-dts-vscode-in-comments.ts b/build/lib/eslint/vscode-dts-vscode-in-comments.ts new file mode 100644 index 0000000000..1410fc2f42 --- /dev/null +++ b/build/lib/eslint/vscode-dts-vscode-in-comments.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * 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 type * as estree from 'estree'; + +export = new class ApiVsCodeInComments implements eslint.Rule.RuleModule { + + readonly meta: eslint.Rule.RuleMetaData = { + messages: { + comment: `Don't use the term 'vs code' in comments` + } + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + + const sourceCode = context.getSourceCode(); + + return { + ['Program']: (_node: any) => { + + for (const comment of sourceCode.getAllComments()) { + if (comment.type !== 'Block') { + continue; + } + if (!comment.range) { + continue; + } + + const startIndex = comment.range[0] + '/*'.length; + const re = /vs code/ig; + let match: RegExpExecArray | null; + while ((match = re.exec(comment.value))) { + // Allow using 'VS Code' in quotes + if (comment.value[match.index - 1] === `'` && comment.value[match.index + match[0].length] === `'`) { + continue; + } + + // Types for eslint seem incorrect + const start = sourceCode.getLocFromIndex(startIndex + match.index) as any as estree.Position; + const end = sourceCode.getLocFromIndex(startIndex + match.index + match[0].length) as any as estree.Position; + context.report({ + messageId: 'comment', + loc: { start, end } + }); + } + } + } + }; + } +}; diff --git a/build/lib/extensions.js b/build/lib/extensions.js index a13ad382b1..20de293871 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -4,9 +4,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.translatePackageJSON = exports.packageRebuildExtensionsStream = exports.cleanRebuildExtensions = exports.packageExternalExtensionsStream = exports.scanBuiltinExtensions = exports.packageMarketplaceExtensionsStream = exports.packageLocalExtensionsStream = exports.vscodeExternalExtensions = exports.fromMarketplace = exports.fromLocalNormal = exports.fromLocal = void 0; +exports.buildExtensionMedia = exports.webpackExtensions = exports.translatePackageJSON = exports.packageRebuildExtensionsStream = exports.cleanRebuildExtensions = exports.packageExternalExtensionsStream = exports.scanBuiltinExtensions = exports.packageMarketplaceExtensionsStream = exports.packageLocalExtensionsStream = exports.vscodeExternalExtensions = exports.fromMarketplace = exports.fromLocalNormal = exports.fromLocal = void 0; const es = require("event-stream"); const fs = require("fs"); +const cp = require("child_process"); const glob = require("glob"); const gulp = require("gulp"); const path = require("path"); @@ -23,7 +24,7 @@ const jsoncParser = require("jsonc-parser"); const util = require('./util'); const root = path.dirname(path.dirname(__dirname)); const commit = util.getVersion(root); -const sourceMappingURLBase = `https://sqlopsbuilds.blob.core.windows.net/sourcemaps/${commit}`; +const sourceMappingURLBase = `https://sqlopsbuilds.blob.core.windows.net/sourcemaps/${commit}`; // {{SQL CARBON EDIT}} function minifyExtensionResources(input) { const jsonFilter = filter(['**/*.json', '**/*.code-snippets'], { restore: true }); return input @@ -144,7 +145,7 @@ function fromLocalWebpack(extensionPath, webpackConfigFileName) { console.error(packagedDependencies); result.emit('error', err); }); - return result.pipe((0, stats_1.createStatsStream)(path.basename(extensionPath))); + return result.pipe(stats_1.createStatsStream(path.basename(extensionPath))); } function fromLocalNormal(extensionPath) { const result = es.through(); @@ -162,7 +163,7 @@ function fromLocalNormal(extensionPath) { es.readArray(files).pipe(result); }) .catch(err => result.emit('error', err)); - return result.pipe((0, stats_1.createStatsStream)(path.basename(extensionPath))); + return result.pipe(stats_1.createStatsStream(path.basename(extensionPath))); } exports.fromLocalNormal = fromLocalNormal; const baseHeaders = { @@ -174,7 +175,7 @@ function fromMarketplace(extensionName, version, metadata) { 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`; + const url = `https://sqlopsextensions.blob.core.windows.net/extensions/${name}/${name}-${version}.vsix`; // {{SQL CARBON EDIT}} fancyLog('Downloading extension:', ansiColors.yellow(`${extensionName}@${version}`), '...'); const options = { base: url, @@ -346,6 +347,7 @@ function scanBuiltinExtensions(extensionsRoot, exclude = []) { } } exports.scanBuiltinExtensions = scanBuiltinExtensions; +// {{SQL CARBON EDIT}} start function packageExternalExtensionsStream() { const extenalExtensionDescriptions = glob.sync('extensions/*/package.json') .map(manifestPath => { @@ -361,7 +363,6 @@ function packageExternalExtensionsStream() { return es.merge(builtExtensions); } exports.packageExternalExtensionsStream = packageExternalExtensionsStream; -// {{SQL CARBON EDIT}} start function cleanRebuildExtensions(root) { return Promise.all(rebuildExtensions.map(async (e) => { await util2.rimraf(path.join(root, e))(); @@ -408,3 +409,132 @@ function translatePackageJSON(packageJSON, packageNLSPath) { return packageJSON; } exports.translatePackageJSON = translatePackageJSON; +const extensionsPath = path.join(root, 'extensions'); +// Additional projects to webpack. These typically build code for webviews +const webpackMediaConfigFiles = [ + 'markdown-language-features/webpack.config.js', + 'simple-browser/webpack.config.js', +]; +// Additional projects to run esbuild on. These typically build code for webviews +const esbuildMediaScripts = [ + 'markdown-language-features/esbuild.js', + 'markdown-math/esbuild.js', +]; +async function webpackExtensions(taskName, isWatch, webpackConfigLocations) { + const webpack = require('webpack'); + const webpackConfigs = []; + for (const { configPath, outputRoot } of webpackConfigLocations) { + const configOrFnOrArray = require(configPath); + function addConfig(configOrFn) { + let config; + if (typeof configOrFn === 'function') { + config = configOrFn({}, {}); + webpackConfigs.push(config); + } + else { + config = configOrFn; + } + if (outputRoot) { + config.output.path = path.join(outputRoot, path.relative(path.dirname(configPath), config.output.path)); + } + webpackConfigs.push(configOrFn); + } + addConfig(configOrFnOrArray); + } + function reporter(fullStats) { + if (Array.isArray(fullStats.children)) { + for (const stats of fullStats.children) { + const outputPath = stats.outputPath; + if (outputPath) { + const relativePath = path.relative(extensionsPath, outputPath).replace(/\\/g, '/'); + const match = relativePath.match(/[^\/]+(\/server|\/client)?/); + fancyLog(`Finished ${ansiColors.green(taskName)} ${ansiColors.cyan(match[0])} with ${stats.errors.length} errors.`); + } + if (Array.isArray(stats.errors)) { + stats.errors.forEach((error) => { + fancyLog.error(error); + }); + } + if (Array.isArray(stats.warnings)) { + stats.warnings.forEach((warning) => { + fancyLog.warn(warning); + }); + } + } + } + } + return new Promise((resolve, reject) => { + if (isWatch) { + webpack(webpackConfigs).watch({}, (err, stats) => { + if (err) { + reject(); + } + else { + reporter(stats.toJson()); + } + }); + } + else { + webpack(webpackConfigs).run((err, stats) => { + if (err) { + fancyLog.error(err); + reject(); + } + else { + reporter(stats.toJson()); + resolve(); + } + }); + } + }); +} +exports.webpackExtensions = webpackExtensions; +async function esbuildExtensions(taskName, isWatch, scripts) { + function reporter(stdError, script) { + const matches = (stdError || '').match(/\> (.+): error: (.+)?/g); + fancyLog(`Finished ${ansiColors.green(taskName)} ${script} with ${matches ? matches.length : 0} errors.`); + for (const match of matches || []) { + fancyLog.error(match); + } + } + const tasks = scripts.map(({ script, outputRoot }) => { + return new Promise((resolve, reject) => { + const args = [script]; + if (isWatch) { + args.push('--watch'); + } + if (outputRoot) { + args.push('--outputRoot', outputRoot); + } + const proc = cp.execFile(process.argv[0], args, {}, (error, _stdout, stderr) => { + if (error) { + return reject(error); + } + reporter(stderr, script); + if (stderr) { + return reject(); + } + return resolve(); + }); + proc.stdout.on('data', (data) => { + fancyLog(`${ansiColors.green(taskName)}: ${data.toString('utf8')}`); + }); + }); + }); + return Promise.all(tasks); +} +async function buildExtensionMedia(isWatch, outputRoot) { + return Promise.all([ + webpackExtensions('webpacking extension media', isWatch, webpackMediaConfigFiles.map(p => { + return { + configPath: path.join(extensionsPath, p), + outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined + }; + })), + esbuildExtensions('esbuilding extension media', isWatch, esbuildMediaScripts.map(p => ({ + script: path.join(extensionsPath, p), + outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined + }))), + ]); +} +exports.buildExtensionMedia = buildExtensionMedia; diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index fa50c88117..1b01a0bea8 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -5,6 +5,7 @@ import * as es from 'event-stream'; import * as fs from 'fs'; +import * as cp from 'child_process'; import * as glob from 'glob'; import * as gulp from 'gulp'; import * as path from 'path'; @@ -19,10 +20,11 @@ import * as fancyLog from 'fancy-log'; import * as ansiColors from 'ansi-colors'; const buffer = require('gulp-buffer'); import * as jsoncParser from 'jsonc-parser'; +import webpack = require('webpack'); const util = require('./util'); const root = path.dirname(path.dirname(__dirname)); const commit = util.getVersion(root); -const sourceMappingURLBase = `https://sqlopsbuilds.blob.core.windows.net/sourcemaps/${commit}`; +const sourceMappingURLBase = `https://sqlopsbuilds.blob.core.windows.net/sourcemaps/${commit}`; // {{SQL CARBON EDIT}} function minifyExtensionResources(input: Stream): Stream { const jsonFilter = filter(['**/*.json', '**/*.code-snippets'], { restore: true }); @@ -205,7 +207,7 @@ export function fromMarketplace(extensionName: string, version: string, metadata 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`; + const url = `https://sqlopsextensions.blob.core.windows.net/extensions/${name}/${name}-${version}.vsix`; // {{SQL CARBON EDIT}} fancyLog('Downloading extension:', ansiColors.yellow(`${extensionName}@${version}`), '...'); @@ -424,6 +426,7 @@ export function scanBuiltinExtensions(extensionsRoot: string, exclude: string[] } } +// {{SQL CARBON EDIT}} start export function packageExternalExtensionsStream(): NodeJS.ReadWriteStream { const extenalExtensionDescriptions = (glob.sync('extensions/*/package.json')) .map(manifestPath => { @@ -441,7 +444,6 @@ export function packageExternalExtensionsStream(): NodeJS.ReadWriteStream { return es.merge(builtExtensions); } -// {{SQL CARBON EDIT}} start export function cleanRebuildExtensions(root: string): Promise { return Promise.all(rebuildExtensions.map(async e => { await util2.rimraf(path.join(root, e))(); @@ -487,3 +489,138 @@ export function translatePackageJSON(packageJSON: string, packageNLSPath: string translate(packageJSON); return packageJSON; } + +const extensionsPath = path.join(root, 'extensions'); + +// Additional projects to webpack. These typically build code for webviews +const webpackMediaConfigFiles = [ + 'markdown-language-features/webpack.config.js', + 'simple-browser/webpack.config.js', +]; + +// Additional projects to run esbuild on. These typically build code for webviews +const esbuildMediaScripts = [ + 'markdown-language-features/esbuild.js', + 'markdown-math/esbuild.js', +]; + +export async function webpackExtensions(taskName: string, isWatch: boolean, webpackConfigLocations: { configPath: string, outputRoot?: string }[]) { + const webpack = require('webpack') as typeof import('webpack'); + + const webpackConfigs: webpack.Configuration[] = []; + + for (const { configPath, outputRoot } of webpackConfigLocations) { + const configOrFnOrArray = require(configPath); + function addConfig(configOrFn: webpack.Configuration | Function) { + let config; + if (typeof configOrFn === 'function') { + config = configOrFn({}, {}); + webpackConfigs.push(config); + } else { + config = configOrFn; + } + + if (outputRoot) { + config.output.path = path.join(outputRoot, path.relative(path.dirname(configPath), config.output.path)); + } + + webpackConfigs.push(configOrFn); + } + addConfig(configOrFnOrArray); + } + function reporter(fullStats: any) { + if (Array.isArray(fullStats.children)) { + for (const stats of fullStats.children) { + const outputPath = stats.outputPath; + if (outputPath) { + const relativePath = path.relative(extensionsPath, outputPath).replace(/\\/g, '/'); + const match = relativePath.match(/[^\/]+(\/server|\/client)?/); + fancyLog(`Finished ${ansiColors.green(taskName)} ${ansiColors.cyan(match![0])} with ${stats.errors.length} errors.`); + } + if (Array.isArray(stats.errors)) { + stats.errors.forEach((error: any) => { + fancyLog.error(error); + }); + } + if (Array.isArray(stats.warnings)) { + stats.warnings.forEach((warning: any) => { + fancyLog.warn(warning); + }); + } + } + } + } + return new Promise((resolve, reject) => { + if (isWatch) { + webpack(webpackConfigs).watch({}, (err, stats) => { + if (err) { + reject(); + } else { + reporter(stats.toJson()); + } + }); + } else { + webpack(webpackConfigs).run((err, stats) => { + if (err) { + fancyLog.error(err); + reject(); + } else { + reporter(stats.toJson()); + resolve(); + } + }); + } + }); +} + +async function esbuildExtensions(taskName: string, isWatch: boolean, scripts: { script: string, outputRoot?: string }[]) { + function reporter(stdError: string, script: string) { + const matches = (stdError || '').match(/\> (.+): error: (.+)?/g); + fancyLog(`Finished ${ansiColors.green(taskName)} ${script} with ${matches ? matches.length : 0} errors.`); + for (const match of matches || []) { + fancyLog.error(match); + } + } + + const tasks = scripts.map(({ script, outputRoot }) => { + return new Promise((resolve, reject) => { + const args = [script]; + if (isWatch) { + args.push('--watch'); + } + if (outputRoot) { + args.push('--outputRoot', outputRoot); + } + const proc = cp.execFile(process.argv[0], args, {}, (error, _stdout, stderr) => { + if (error) { + return reject(error); + } + reporter(stderr, script); + if (stderr) { + return reject(); + } + return resolve(); + }); + + proc.stdout!.on('data', (data) => { + fancyLog(`${ansiColors.green(taskName)}: ${data.toString('utf8')}`); + }); + }); + }); + return Promise.all(tasks); +} + +export async function buildExtensionMedia(isWatch: boolean, outputRoot?: string) { + return Promise.all([ + webpackExtensions('webpacking extension media', isWatch, webpackMediaConfigFiles.map(p => { + return { + configPath: path.join(extensionsPath, p), + outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined + }; + })), + esbuildExtensions('esbuilding extension media', isWatch, esbuildMediaScripts.map(p => ({ + script: path.join(extensionsPath, p), + outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined + }))), + ]); +} diff --git a/build/lib/i18n.js b/build/lib/i18n.js index ad14ef33a8..5c4ce69ff9 100644 --- a/build/lib/i18n.js +++ b/build/lib/i18n.js @@ -4,14 +4,13 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.prepareIslFiles = exports.prepareI18nPackFiles = exports.pullI18nPackFiles = exports.i18nPackVersion = exports.createI18nFile = exports.prepareI18nFiles = exports.pullSetupXlfFiles = exports.pullCoreAndExtensionsXlfFiles = exports.findObsoleteResources = exports.pushXlfFiles = exports.createXlfFilesForIsl = exports.createXlfFilesForExtensions = exports.createXlfFilesForCoreBundle = exports.getResource = exports.processNlsFiles = exports.Limiter = exports.XLF = exports.Line = exports.externalExtensionsWithTranslations = exports.extraLanguages = exports.defaultLanguages = void 0; +exports.prepareIslFiles = exports.prepareI18nPackFiles = exports.i18nPackVersion = exports.createI18nFile = exports.prepareI18nFiles = exports.pullSetupXlfFiles = exports.findObsoleteResources = exports.pushXlfFiles = exports.createXlfFilesForIsl = exports.createXlfFilesForExtensions = exports.createXlfFilesForCoreBundle = exports.getResource = exports.processNlsFiles = exports.Limiter = exports.XLF = exports.Line = exports.externalExtensionsWithTranslations = exports.extraLanguages = exports.defaultLanguages = void 0; const path = require("path"); const fs = require("fs"); const event_stream_1 = require("event-stream"); const File = require("vinyl"); const Is = require("is"); const xml2js = require("xml2js"); -const glob = require("glob"); const https = require("https"); const gulp = require("gulp"); const fancyLog = require("fancy-log"); @@ -110,12 +109,16 @@ class XLF { } toString() { this.appendHeader(); - for (let file in this.files) { + const files = Object.keys(this.files).sort(); + for (const file of files) { this.appendNewLine(``, 2); - for (let item of this.files[file]) { + const items = this.files[file].sort((a, b) => { + return a.id < b.id ? -1 : a.id > b.id ? 1 : 0; + }); + for (const item of items) { this.addStringItem(file, item); } - this.appendNewLine('', 2); + this.appendNewLine(''); } this.appendFooter(); return this.buffer.join('\r\n'); @@ -463,7 +466,7 @@ function processCoreBundleFormat(fileHeader, languages, json, emitter) { }); } function processNlsFiles(opts) { - return (0, event_stream_1.through)(function (file) { + return event_stream_1.through(function (file) { let fileName = path.basename(file.path); if (fileName === 'nls.metadata.json') { let json = null; @@ -521,7 +524,7 @@ function getResource(sourceFile) { } exports.getResource = getResource; function createXlfFilesForCoreBundle() { - return (0, event_stream_1.through)(function (file) { + return event_stream_1.through(function (file) { const basename = path.basename(file.path); if (basename === 'nls.metadata.json') { if (file.isBuffer()) { @@ -576,7 +579,7 @@ function createXlfFilesForExtensions() { let counter = 0; let folderStreamEnded = false; let folderStreamEndEmitted = false; - return (0, event_stream_1.through)(function (extensionFolder) { + return event_stream_1.through(function (extensionFolder) { const folderStream = this; const stat = fs.statSync(extensionFolder.path); if (!stat.isDirectory()) { @@ -594,7 +597,7 @@ function createXlfFilesForExtensions() { } return _xlf; } - gulp.src([`.build/extensions/${extensionName}/package.nls.json`, `.build/extensions/${extensionName}/**/nls.metadata.json`], { allowEmpty: true }).pipe((0, event_stream_1.through)(function (file) { + gulp.src([`.build/extensions/${extensionName}/package.nls.json`, `.build/extensions/${extensionName}/**/nls.metadata.json`], { allowEmpty: true }).pipe(event_stream_1.through(function (file) { if (file.isBuffer()) { const buffer = file.contents; const basename = path.basename(file.path); @@ -653,15 +656,14 @@ function createXlfFilesForExtensions() { } exports.createXlfFilesForExtensions = createXlfFilesForExtensions; function createXlfFilesForIsl() { - return (0, event_stream_1.through)(function (file) { + return event_stream_1.through(function (file) { let projectName, resourceFile; - if (path.basename(file.path) === 'Default.isl') { + if (path.basename(file.path) === 'messages.en.isl') { projectName = setupProject; - resourceFile = 'setup_default.xlf'; + resourceFile = 'messages.xlf'; } else { - projectName = workbenchProject; - resourceFile = 'setup_messages.xlf'; + throw new Error(`Unknown input file ${file.path}`); } let xlf = new XLF(projectName), keys = [], messages = []; let model = new TextModel(file.contents.toString()); @@ -707,7 +709,7 @@ exports.createXlfFilesForIsl = createXlfFilesForIsl; function pushXlfFiles(apiHostname, username, password) { let tryGetPromises = []; let updateCreatePromises = []; - return (0, event_stream_1.through)(function (file) { + return event_stream_1.through(function (file) { const project = path.dirname(file.relative); const fileName = path.basename(file.path); const slug = fileName.substr(0, fileName.length - '.xlf'.length); @@ -769,7 +771,7 @@ function getAllResources(project, apiHostname, username, password) { function findObsoleteResources(apiHostname, username, password) { let resourcesByProject = Object.create(null); resourcesByProject[extensionsProject] = [].concat(exports.externalExtensionsWithTranslations); // clone - return (0, event_stream_1.through)(function (file) { + return event_stream_1.through(function (file) { const project = path.dirname(file.relative); const fileName = path.basename(file.path); const slug = fileName.substr(0, fileName.length - '.xlf'.length); @@ -909,31 +911,6 @@ function updateResource(project, slug, xlfFile, apiHostname, credentials) { request.end(); }); } -// cache resources -let _coreAndExtensionResources; -function pullCoreAndExtensionsXlfFiles(apiHostname, username, password, language, externalExtensions) { - if (!_coreAndExtensionResources) { - _coreAndExtensionResources = []; - // editor and workbench - const json = JSON.parse(fs.readFileSync('./build/lib/i18n.resources.json', 'utf8')); - _coreAndExtensionResources.push(...json.editor); - _coreAndExtensionResources.push(...json.workbench); - // extensions - let extensionsToLocalize = Object.create(null); - glob.sync('.build/extensions/**/*.nls.json').forEach(extension => extensionsToLocalize[extension.split('/')[2]] = true); - glob.sync('.build/extensions/*/node_modules/vscode-nls').forEach(extension => extensionsToLocalize[extension.split('/')[2]] = true); - Object.keys(extensionsToLocalize).forEach(extension => { - _coreAndExtensionResources.push({ name: extension, project: extensionsProject }); - }); - if (externalExtensions) { - for (let resourceName in externalExtensions) { - _coreAndExtensionResources.push({ name: resourceName, project: extensionsProject }); - } - } - } - return pullXlfFiles(apiHostname, username, password, language, _coreAndExtensionResources); -} -exports.pullCoreAndExtensionsXlfFiles = pullCoreAndExtensionsXlfFiles; function pullSetupXlfFiles(apiHostname, username, password, language, includeDefault) { let setupResources = [{ name: 'setup_messages', project: workbenchProject }]; if (includeDefault) { @@ -946,7 +923,7 @@ function pullXlfFiles(apiHostname, username, password, language, resources) { const credentials = `${username}:${password}`; let expectedTranslationsCount = resources.length; let translationsRetrieved = 0, called = false; - return (0, event_stream_1.readable)(function (_count, callback) { + return event_stream_1.readable(function (_count, callback) { // Mark end of stream when all resources were retrieved if (translationsRetrieved === expectedTranslationsCount) { return this.emit('end'); @@ -1004,7 +981,7 @@ function retrieveResource(language, resource, apiHostname, credentials) { } function prepareI18nFiles() { let parsePromises = []; - return (0, event_stream_1.through)(function (xlf) { + return event_stream_1.through(function (xlf) { let stream = this; let parsePromise = XLF.parse(xlf.contents.toString()); parsePromises.push(parsePromise); @@ -1044,20 +1021,16 @@ function createI18nFile(originalFilePath, messages) { } exports.createI18nFile = createI18nFile; exports.i18nPackVersion = '1.0.0'; // {{SQL CARBON EDIT}} Needed in locfunc. -function pullI18nPackFiles(apiHostname, username, password, language, resultingTranslationPaths) { - return pullCoreAndExtensionsXlfFiles(apiHostname, username, password, language, exports.externalExtensionsWithTranslations) - .pipe(prepareI18nPackFiles(exports.externalExtensionsWithTranslations, resultingTranslationPaths, language.id === 'ps')); -} -exports.pullI18nPackFiles = pullI18nPackFiles; function prepareI18nPackFiles(externalExtensions, resultingTranslationPaths, pseudo = false) { let parsePromises = []; let mainPack = { version: exports.i18nPackVersion, contents: {} }; let extensionsPacks = {}; let errors = []; - return (0, event_stream_1.through)(function (xlf) { - let project = path.basename(path.dirname(xlf.relative)); + return event_stream_1.through(function (xlf) { + let project = path.basename(path.dirname(path.dirname(xlf.relative))); let resource = path.basename(xlf.relative, '.xlf'); let contents = xlf.contents.toString(); + log(`Found ${project}: ${resource}`); let parsePromise = pseudo ? XLF.parsePseudo(contents) : XLF.parse(contents); parsePromises.push(parsePromise); parsePromise.then(resolvedFiles => { @@ -1115,15 +1088,12 @@ function prepareI18nPackFiles(externalExtensions, resultingTranslationPaths, pse exports.prepareI18nPackFiles = prepareI18nPackFiles; function prepareIslFiles(language, innoSetupConfig) { let parsePromises = []; - return (0, event_stream_1.through)(function (xlf) { + return event_stream_1.through(function (xlf) { let stream = this; let parsePromise = XLF.parse(xlf.contents.toString()); parsePromises.push(parsePromise); parsePromise.then(resolvedFiles => { resolvedFiles.forEach(file => { - if (path.basename(file.originalFilePath) === 'Default' && !innoSetupConfig.defaultInfo) { - return; - } let translatedFile = createIslFile(file.originalFilePath, file.messages, language, innoSetupConfig); stream.queue(translatedFile); }); @@ -1159,20 +1129,9 @@ function createIslFile(originalFilePath, messages, language, innoSetup) { let key = sections[0]; let translated = line; if (key) { - if (key === 'LanguageName') { - translated = `${key}=${innoSetup.defaultInfo.name}`; - } - else if (key === 'LanguageID') { - translated = `${key}=${innoSetup.defaultInfo.id}`; - } - else if (key === 'LanguageCodePage') { - translated = `${key}=${innoSetup.codePage.substr(2)}`; - } - else { - let translatedMessage = messages[key]; - if (translatedMessage) { - translated = `${key}=${translatedMessage}`; - } + let translatedMessage = messages[key]; + if (translatedMessage) { + translated = `${key}=${translatedMessage}`; } } content.push(translated); diff --git a/build/lib/i18n.ts b/build/lib/i18n.ts index 7390a889b7..2593697acb 100644 --- a/build/lib/i18n.ts +++ b/build/lib/i18n.ts @@ -10,7 +10,6 @@ import { through, readable, ThroughStream } from 'event-stream'; import * as File from 'vinyl'; import * as Is from 'is'; import * as xml2js from 'xml2js'; -import * as glob from 'glob'; import * as https from 'https'; import * as gulp from 'gulp'; import * as fancyLog from 'fancy-log'; @@ -31,10 +30,6 @@ export interface Language { export interface InnoSetup { codePage: string; //code page for encoding (http://www.jrsoftware.org/ishelp/index.php?topic=langoptionssection) - defaultInfo?: { - name: string; // inno setup language name - id: string; // locale identifier (https://msdn.microsoft.com/en-us/library/dd318693.aspx) - }; } export const defaultLanguages: Language[] = [ @@ -198,14 +193,17 @@ export class XLF { public toString(): string { this.appendHeader(); - for (let file in this.files) { + const files = Object.keys(this.files).sort(); + for (const file of files) { this.appendNewLine(``, 2); - for (let item of this.files[file]) { + const items = this.files[file].sort((a: Item, b: Item) => { + return a.id < b.id ? -1 : a.id > b.id ? 1 : 0; + }); + for (const item of items) { this.addStringItem(file, item); } - this.appendNewLine('', 2); + this.appendNewLine(''); } - this.appendFooter(); return this.buffer.join('\r\n'); } @@ -775,12 +773,11 @@ export function createXlfFilesForIsl(): ThroughStream { return through(function (this: ThroughStream, file: File) { let projectName: string, resourceFile: string; - if (path.basename(file.path) === 'Default.isl') { + if (path.basename(file.path) === 'messages.en.isl') { projectName = setupProject; - resourceFile = 'setup_default.xlf'; + resourceFile = 'messages.xlf'; } else { - projectName = workbenchProject; - resourceFile = 'setup_messages.xlf'; + throw new Error(`Unknown input file ${file.path}`); } let xlf = new XLF(projectName), @@ -1048,35 +1045,6 @@ function updateResource(project: string, slug: string, xlfFile: File, apiHostnam }); } -// cache resources -let _coreAndExtensionResources: Resource[]; - -export function pullCoreAndExtensionsXlfFiles(apiHostname: string, username: string, password: string, language: Language, externalExtensions?: Map): NodeJS.ReadableStream { - if (!_coreAndExtensionResources) { - _coreAndExtensionResources = []; - // editor and workbench - const json = JSON.parse(fs.readFileSync('./build/lib/i18n.resources.json', 'utf8')); - _coreAndExtensionResources.push(...json.editor); - _coreAndExtensionResources.push(...json.workbench); - - // extensions - let extensionsToLocalize = Object.create(null); - glob.sync('.build/extensions/**/*.nls.json').forEach(extension => extensionsToLocalize[extension.split('/')[2]] = true); - glob.sync('.build/extensions/*/node_modules/vscode-nls').forEach(extension => extensionsToLocalize[extension.split('/')[2]] = true); - - Object.keys(extensionsToLocalize).forEach(extension => { - _coreAndExtensionResources.push({ name: extension, project: extensionsProject }); - }); - - if (externalExtensions) { - for (let resourceName in externalExtensions) { - _coreAndExtensionResources.push({ name: resourceName, project: extensionsProject }); - } - } - } - return pullXlfFiles(apiHostname, username, password, language, _coreAndExtensionResources); -} - export function pullSetupXlfFiles(apiHostname: string, username: string, password: string, language: Language, includeDefault: boolean): NodeJS.ReadableStream { let setupResources = [{ name: 'setup_messages', project: workbenchProject }]; if (includeDefault) { @@ -1208,20 +1176,16 @@ export interface TranslationPath { resourceName: string; } -export function pullI18nPackFiles(apiHostname: string, username: string, password: string, language: Language, resultingTranslationPaths: TranslationPath[]): NodeJS.ReadableStream { - return pullCoreAndExtensionsXlfFiles(apiHostname, username, password, language, externalExtensionsWithTranslations) - .pipe(prepareI18nPackFiles(externalExtensionsWithTranslations, resultingTranslationPaths, language.id === 'ps')); -} - export function prepareI18nPackFiles(externalExtensions: Map, resultingTranslationPaths: TranslationPath[], pseudo = false): NodeJS.ReadWriteStream { let parsePromises: Promise[] = []; let mainPack: I18nPack = { version: i18nPackVersion, contents: {} }; let extensionsPacks: Map = {}; let errors: any[] = []; return through(function (this: ThroughStream, xlf: File) { - let project = path.basename(path.dirname(xlf.relative)); + let project = path.basename(path.dirname(path.dirname(xlf.relative))); let resource = path.basename(xlf.relative, '.xlf'); let contents = xlf.contents.toString(); + log(`Found ${project}: ${resource}`); let parsePromise = pseudo ? XLF.parsePseudo(contents) : XLF.parse(contents); parsePromises.push(parsePromise); parsePromise.then( @@ -1290,9 +1254,6 @@ export function prepareIslFiles(language: Language, innoSetupConfig: InnoSetup): parsePromise.then( resolvedFiles => { resolvedFiles.forEach(file => { - if (path.basename(file.originalFilePath) === 'Default' && !innoSetupConfig.defaultInfo) { - return; - } let translatedFile = createIslFile(file.originalFilePath, file.messages, language, innoSetupConfig); stream.queue(translatedFile); }); @@ -1327,17 +1288,9 @@ function createIslFile(originalFilePath: string, messages: Map, language let key = sections[0]; let translated = line; if (key) { - if (key === 'LanguageName') { - translated = `${key}=${innoSetup.defaultInfo!.name}`; - } else if (key === 'LanguageID') { - translated = `${key}=${innoSetup.defaultInfo!.id}`; - } else if (key === 'LanguageCodePage') { - translated = `${key}=${innoSetup.codePage.substr(2)}`; - } else { - let translatedMessage = messages[key]; - if (translatedMessage) { - translated = `${key}=${translatedMessage}`; - } + let translatedMessage = messages[key]; + if (translatedMessage) { + translated = `${key}=${translatedMessage}`; } } diff --git a/build/lib/layersChecker.js b/build/lib/layersChecker.js index a500bb3136..76b800761b 100644 --- a/build/lib/layersChecker.js +++ b/build/lib/layersChecker.js @@ -199,7 +199,7 @@ const RULES = [ ] } ]; -const TS_CONFIG_PATH = (0, path_1.join)(__dirname, '../../', 'src', 'tsconfig.json'); +const TS_CONFIG_PATH = path_1.join(__dirname, '../../', 'src', 'tsconfig.json'); let hasErrors = false; function checkFile(program, sourceFile, rule) { checkNode(sourceFile); @@ -250,8 +250,8 @@ function checkFile(program, sourceFile, rule) { } function createProgram(tsconfigPath) { const tsConfig = ts.readConfigFile(tsconfigPath, ts.sys.readFile); - const configHostParser = { fileExists: fs_1.existsSync, readDirectory: ts.sys.readDirectory, readFile: file => (0, fs_1.readFileSync)(file, 'utf8'), useCaseSensitiveFileNames: process.platform === 'linux' }; - const tsConfigParsed = ts.parseJsonConfigFileContent(tsConfig.config, configHostParser, (0, path_1.resolve)((0, path_1.dirname)(tsconfigPath)), { noEmit: true }); + const configHostParser = { fileExists: fs_1.existsSync, readDirectory: ts.sys.readDirectory, readFile: file => fs_1.readFileSync(file, 'utf8'), useCaseSensitiveFileNames: process.platform === 'linux' }; + const tsConfigParsed = ts.parseJsonConfigFileContent(tsConfig.config, configHostParser, path_1.resolve(path_1.dirname(tsconfigPath)), { noEmit: true }); const compilerHost = ts.createCompilerHost(tsConfigParsed.options, true); return ts.createProgram(tsConfigParsed.fileNames, tsConfigParsed.options, compilerHost); } @@ -261,7 +261,7 @@ function createProgram(tsconfigPath) { const program = createProgram(TS_CONFIG_PATH); for (const sourceFile of program.getSourceFiles()) { for (const rule of RULES) { - if ((0, minimatch_1.match)([sourceFile.fileName], rule.target).length > 0) { + if (minimatch_1.match([sourceFile.fileName], rule.target).length > 0) { if (!rule.skip) { checkFile(program, sourceFile, rule); } diff --git a/build/lib/locFunc.js b/build/lib/locFunc.js index ac74e8bdfa..5356f5cc82 100644 --- a/build/lib/locFunc.js +++ b/build/lib/locFunc.js @@ -96,7 +96,7 @@ function modifyI18nPackFiles(existingTranslationFolder, resultingTranslationPath let mainPack = { version: i18n.i18nPackVersion, contents: {} }; let extensionsPacks = {}; let errors = []; - return (0, event_stream_1.through)(function (xlf) { + return event_stream_1.through(function (xlf) { let rawResource = path.basename(xlf.relative, '.xlf'); let resource = rawResource.substring(0, rawResource.lastIndexOf('.')); let contents = xlf.contents.toString(); diff --git a/build/lib/nls.js b/build/lib/nls.js index 88d8973422..710d3061c3 100644 --- a/build/lib/nls.js +++ b/build/lib/nls.js @@ -53,8 +53,8 @@ define([], [${wrap + lines.map(l => indent + l).join(',\n') + wrap}]);`; * Returns a stream containing the patched JavaScript and source maps. */ function nls() { - const input = (0, event_stream_1.through)(); - const output = input.pipe((0, event_stream_1.through)(function (f) { + const input = event_stream_1.through(); + const output = input.pipe(event_stream_1.through(function (f) { if (!f.sourceMap) { return this.emit('error', new Error(`File ${f.relative} does not have sourcemaps.`)); } @@ -72,7 +72,7 @@ function nls() { } _nls.patchFiles(f, typescript).forEach(f => this.emit('data', f)); })); - return (0, event_stream_1.duplex)(input, output); + return event_stream_1.duplex(input, output); } exports.nls = nls; function isImportNode(ts, node) { diff --git a/build/lib/optimize.js b/build/lib/optimize.js index e708e82121..6b3d87f83f 100644 --- a/build/lib/optimize.js +++ b/build/lib/optimize.js @@ -98,7 +98,7 @@ function toConcatStream(src, bundledFileHeader, sources, dest, fileContentMapper return es.readArray(treatedSources) .pipe(useSourcemaps ? util.loadSourcemaps() : es.through()) .pipe(concat(dest)) - .pipe((0, stats_1.createStatsStream)(dest)); + .pipe(stats_1.createStatsStream(dest)); } function toBundleStream(src, bundledFileHeader, bundles, fileContentMapper) { return es.merge(bundles.map(function (bundle) { @@ -155,7 +155,7 @@ function optimizeTask(opts) { addComment: true, includeContent: true })) - .pipe(opts.languages && opts.languages.length ? (0, i18n_1.processNlsFiles)({ + .pipe(opts.languages && opts.languages.length ? i18n_1.processNlsFiles({ fileHeader: bundledFileHeader, languages: opts.languages }) : es.through()) @@ -179,7 +179,7 @@ function minifyTask(src, sourceMapBaseUrl) { sourcemap: 'external', outdir: '.', platform: 'node', - target: ['node12.18'], + target: ['node14.16'], write: false }).then(res => { const jsFile = res.outputFiles.find(f => /\.js$/.test(f.path)); diff --git a/build/lib/optimize.ts b/build/lib/optimize.ts index aa04891d17..501a45c41c 100644 --- a/build/lib/optimize.ts +++ b/build/lib/optimize.ts @@ -256,7 +256,7 @@ export function minifyTask(src: string, sourceMapBaseUrl?: string): (cb: any) => sourcemap: 'external', outdir: '.', platform: 'node', - target: ['node12.18'], + target: ['node14.16'], write: false }).then(res => { const jsFile = res.outputFiles.find(f => /\.js$/.test(f.path))!; diff --git a/build/lib/preLaunch.js b/build/lib/preLaunch.js index 5cfce3e39d..e7b38cac36 100644 --- a/build/lib/preLaunch.js +++ b/build/lib/preLaunch.js @@ -12,7 +12,7 @@ const yarn = process.platform === 'win32' ? 'yarn.cmd' : 'yarn'; const rootDir = path.resolve(__dirname, '..', '..'); function runProcess(command, args = []) { return new Promise((resolve, reject) => { - const child = (0, child_process_1.spawn)(command, args, { cwd: rootDir, stdio: 'inherit', env: process.env }); + const child = child_process_1.spawn(command, args, { cwd: rootDir, stdio: 'inherit', env: process.env }); child.on('exit', err => !err ? resolve() : process.exit(err !== null && err !== void 0 ? err : 1)); child.on('error', reject); }); diff --git a/build/lib/standalone.js b/build/lib/standalone.js index 42d3d3aa39..027ee9ee63 100644 --- a/build/lib/standalone.js +++ b/build/lib/standalone.js @@ -260,7 +260,7 @@ function transportCSS(module, enqueue, write) { } const filename = path.join(SRC_DIR, module); const fileContents = fs.readFileSync(filename).toString(); - const inlineResources = 'base64'; // see https://github.com/Microsoft/monaco-editor/issues/148 + const inlineResources = 'base64'; // see https://github.com/microsoft/monaco-editor/issues/148 const newContents = _rewriteOrInlineUrls(fileContents, inlineResources === 'base64'); write(module, newContents); return true; diff --git a/build/lib/standalone.ts b/build/lib/standalone.ts index d55cdd5497..92637c2b12 100644 --- a/build/lib/standalone.ts +++ b/build/lib/standalone.ts @@ -302,7 +302,7 @@ function transportCSS(module: string, enqueue: (module: string) => void, write: const filename = path.join(SRC_DIR, module); const fileContents = fs.readFileSync(filename).toString(); - const inlineResources = 'base64'; // see https://github.com/Microsoft/monaco-editor/issues/148 + const inlineResources = 'base64'; // see https://github.com/microsoft/monaco-editor/issues/148 const newContents = _rewriteOrInlineUrls(fileContents, inlineResources === 'base64'); write(module, newContents); diff --git a/build/lib/treeshaking.js b/build/lib/treeshaking.js index 0e98826b88..d7e91243d0 100644 --- a/build/lib/treeshaking.js +++ b/build/lib/treeshaking.js @@ -241,6 +241,9 @@ function nodeOrChildIsBlack(node) { } return false; } +function isSymbolWithDeclarations(symbol) { + return !!(symbol && symbol.declarations); +} function markNodes(ts, languageService, options) { const program = languageService.getProgram(); if (!program) { @@ -413,7 +416,7 @@ function markNodes(ts, languageService, options) { if (symbolImportNode) { setColor(symbolImportNode, 2 /* Black */); } - if (symbol && !nodeIsInItsOwnDeclaration(nodeSourceFile, node, symbol)) { + if (isSymbolWithDeclarations(symbol) && !nodeIsInItsOwnDeclaration(nodeSourceFile, node, symbol)) { for (let i = 0, len = symbol.declarations.length; i < len; i++) { // {{SQL CARBON EDIT}} Compile fixes const declaration = symbol.declarations[i]; // {{SQL CARBON EDIT}} Compile fixes if (ts.isSourceFile(declaration)) { @@ -686,7 +689,7 @@ function getRealNodeSymbol(ts, checker, node) { // get the aliased symbol instead. This allows for goto def on an import e.g. // import {A, B} from "mod"; // to jump to the implementation directly. - if (symbol && symbol.flags & ts.SymbolFlags.Alias && shouldSkipAlias(node, symbol.declarations[0])) { // {{SQL CARBON EDIT}} Compile fixes + if (symbol && symbol.flags & ts.SymbolFlags.Alias && symbol.declarations && shouldSkipAlias(node, symbol.declarations[0])) { // {{SQL CARBON EDIT}} Compile fixes const aliased = checker.getAliasedSymbol(symbol); if (aliased.declarations) { // We should mark the import as visited diff --git a/build/lib/treeshaking.ts b/build/lib/treeshaking.ts index ea0a950018..564a492dd2 100644 --- a/build/lib/treeshaking.ts +++ b/build/lib/treeshaking.ts @@ -323,6 +323,10 @@ function nodeOrChildIsBlack(node: ts.Node): boolean { return false; } +function isSymbolWithDeclarations(symbol: ts.Symbol | undefined | null): symbol is ts.Symbol & { declarations: ts.Declaration[] } { + return !!(symbol && symbol.declarations); +} + function markNodes(ts: typeof import('typescript'), languageService: ts.LanguageService, options: ITreeShakingOptions) { const program = languageService.getProgram(); if (!program) { @@ -530,7 +534,7 @@ function markNodes(ts: typeof import('typescript'), languageService: ts.Language setColor(symbolImportNode, NodeColor.Black); } - if (symbol && !nodeIsInItsOwnDeclaration(nodeSourceFile, node, symbol)) { + if (isSymbolWithDeclarations(symbol) && !nodeIsInItsOwnDeclaration(nodeSourceFile, node, symbol)) { for (let i = 0, len = symbol.declarations!.length; i < len; i++) { // {{SQL CARBON EDIT}} Compile fixes const declaration = symbol.declarations![i]; // {{SQL CARBON EDIT}} Compile fixes if (ts.isSourceFile(declaration)) { @@ -595,7 +599,7 @@ function markNodes(ts: typeof import('typescript'), languageService: ts.Language } } -function nodeIsInItsOwnDeclaration(nodeSourceFile: ts.SourceFile, node: ts.Node, symbol: ts.Symbol): boolean { +function nodeIsInItsOwnDeclaration(nodeSourceFile: ts.SourceFile, node: ts.Node, symbol: ts.Symbol & { declarations: ts.Declaration[] }): boolean { for (let i = 0, len = symbol.declarations!.length; i < len; i++) { // {{SQL CARBON EDIT}} Compile fixes const declaration = symbol.declarations![i]; // {{SQL CARBON EDIT}} Compile fixes const declarationSourceFile = declaration.getSourceFile(); @@ -838,7 +842,7 @@ function getRealNodeSymbol(ts: typeof import('typescript'), checker: ts.TypeChec // get the aliased symbol instead. This allows for goto def on an import e.g. // import {A, B} from "mod"; // to jump to the implementation directly. - if (symbol && symbol.flags & ts.SymbolFlags.Alias && shouldSkipAlias(node, symbol.declarations![0])) { // {{SQL CARBON EDIT}} Compile fixes + if (symbol && symbol.flags & ts.SymbolFlags.Alias && symbol.declarations && shouldSkipAlias(node, symbol.declarations![0])) { // {{SQL CARBON EDIT}} Compile fixes const aliased = checker.getAliasedSymbol(symbol); if (aliased.declarations) { // We should mark the import as visited diff --git a/build/lib/typings/gulp-bom.d.ts b/build/lib/typings/gulp-bom.d.ts index 94dc5fd6d2..88525a7e9d 100644 --- a/build/lib/typings/gulp-bom.d.ts +++ b/build/lib/typings/gulp-bom.d.ts @@ -4,7 +4,7 @@ declare module "gulp-bom" { /** * This is required as per: - * https://github.com/Microsoft/TypeScript/issues/5073 + * https://github.com/microsoft/TypeScript/issues/5073 */ namespace f {} diff --git a/build/lib/typings/gulp-flatmap.d.ts b/build/lib/typings/gulp-flatmap.d.ts index 82dd84e15b..c99232c61c 100644 --- a/build/lib/typings/gulp-flatmap.d.ts +++ b/build/lib/typings/gulp-flatmap.d.ts @@ -4,9 +4,9 @@ declare module 'gulp-flatmap' { /** * This is required as per: - * https://github.com/Microsoft/TypeScript/issues/5073 + * https://github.com/microsoft/TypeScript/issues/5073 */ namespace f {} export = f; -} \ No newline at end of file +} diff --git a/build/lib/typings/vinyl.d.ts b/build/lib/typings/vinyl.d.ts index a85632e172..6be30a1eeb 100644 --- a/build/lib/typings/vinyl.d.ts +++ b/build/lib/typings/vinyl.d.ts @@ -103,10 +103,10 @@ declare module "vinyl" { /** * This is required as per: - * https://github.com/Microsoft/TypeScript/issues/5073 + * https://github.com/microsoft/TypeScript/issues/5073 */ namespace File {} export = File; -} \ No newline at end of file +} diff --git a/build/monaco/package.json b/build/monaco/package.json index 446fea6051..9a6e05c123 100644 --- a/build/monaco/package.json +++ b/build/monaco/package.json @@ -9,9 +9,9 @@ "module": "./esm/vs/editor/editor.main.js", "repository": { "type": "git", - "url": "https://github.com/Microsoft/vscode" + "url": "https://github.com/microsoft/vscode" }, "bugs": { - "url": "https://github.com/Microsoft/vscode/issues" + "url": "https://github.com/microsoft/vscode/issues" } } diff --git a/build/npm/dirs.js b/build/npm/dirs.js index e40e03258d..ba4e8a47a3 100644 --- a/build/npm/dirs.js +++ b/build/npm/dirs.js @@ -9,39 +9,52 @@ exports.dirs = [ 'build', 'build/lib/watch', 'extensions', + // {{SQL CARBON EDIT}} Add ADS extensions and remove VSCode ones + 'extensions/admin-tool-ext-win', + 'extensions/agent', + 'extensions/arc', + 'extensions/azcli', + 'extensions/azurecore', + 'extensions/azurehybridtoolkit', + 'extensions/azuremonitor', + 'extensions/big-data-cluster', + 'extensions/cms', 'extensions/configuration-editing', - 'extensions/css-language-features', - 'extensions/css-language-features/server', - 'extensions/debug-auto-launch', - 'extensions/debug-server-ready', - 'extensions/emmet', + 'extensions/dacpac', + 'extensions/data-workspace', 'extensions/extension-editing', 'extensions/git', 'extensions/github', 'extensions/github-authentication', - 'extensions/grunt', - 'extensions/gulp', - 'extensions/html-language-features', - 'extensions/html-language-features/server', 'extensions/image-preview', - 'extensions/jake', + 'extensions/import', + 'extensions/integration-tests', 'extensions/json-language-features', 'extensions/json-language-features/server', + 'extensions/kusto', + 'extensions/liveshare', + 'extensions/machine-learning', 'extensions/markdown-language-features', + 'extensions/markdown-math', 'extensions/merge-conflict', 'extensions/microsoft-authentication', - 'extensions/notebook-markdown-extensions', - 'extensions/npm', - 'extensions/php-language-features', + 'extensions/mssql', + 'extensions/notebook', + 'extensions/profiler', + 'extensions/python', + 'extensions/query-history', + 'extensions/resource-deployment', + 'extensions/schema-compare', 'extensions/search-result', + 'extensions/server-report', 'extensions/simple-browser', + 'extensions/sql-assessment', + 'extensions/sql-database-projects', + 'extensions/sql-migration', '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', + 'extensions/xml-language-features', + // {{SQL CARBON EDIT}} - End 'remote', 'remote/web', 'test/automation', diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js index ff9bb5498b..bb3bf091f3 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -6,6 +6,7 @@ const cp = require('child_process'); const path = require('path'); const fs = require('fs'); +const { dirs } = require('./dirs'); const yarn = process.platform === 'win32' ? 'yarn.cmd' : 'yarn'; /** @@ -21,6 +22,10 @@ function yarnInstall(location, opts) { const argv = JSON.parse(raw); const original = argv.original || []; const args = original.filter(arg => arg === '--ignore-optional' || arg === '--frozen-lockfile'); + if (opts.ignoreEngines) { + args.push('--ignore-engines'); + delete opts.ignoreEngines; + } console.log(`Installing dependencies in ${location}...`); console.log(`$ yarn ${args.join(' ')}`); @@ -31,24 +36,39 @@ function yarnInstall(location, opts) { } } -yarnInstall('extensions'); // node modules shared by all extensions +for (let dir of dirs) { -if (!(process.platform === 'win32' && (process.arch === 'arm64' || process.env['npm_config_arch'] === 'arm64'))) { - yarnInstall('remote'); // node modules used by vscode server - yarnInstall('remote/web'); // node modules used by vscode web -} - -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.dependencies || packageJSON.devDependencies); - } catch (e) { - return false; + if (dir === '') { + // `yarn` already executed in root + continue; } -}); -extensions.forEach(extension => yarnInstall(`extensions/${extension}`)); + if (/^remote/.test(dir) && process.platform === 'win32' && (process.arch === 'arm64' || process.env['npm_config_arch'] === 'arm64')) { + // windows arm: do not execute `yarn` on remote folder + continue; + } + + if (dir === 'build/lib/watch') { + // node modules for watching, specific to host node version, not electron + yarnInstallBuildDependencies(); + continue; + } + + let opts; + + if (dir === 'remote') { + // node modules used by vscode server + const env = { ...process.env }; + if (process.env['VSCODE_REMOTE_CC']) { env['CC'] = process.env['VSCODE_REMOTE_CC']; } + if (process.env['VSCODE_REMOTE_CXX']) { env['CXX'] = process.env['VSCODE_REMOTE_CXX']; } + if (process.env['VSCODE_REMOTE_NODE_GYP']) { env['npm_config_node_gyp'] = process.env['VSCODE_REMOTE_NODE_GYP']; } + opts = { env }; + } else if (/^extensions\//.test(dir)) { + opts = { ignoreEngines: true }; + } + + yarnInstall(dir, opts); +} function yarnInstallBuildDependencies() { // make sure we install the deps of build/lib/watch for the system installed @@ -68,10 +88,4 @@ runtime "${runtime}"`; yarnInstall(watchPath); } -yarnInstall(`build`); // node modules required for build -yarnInstall('test/automation'); // node modules required for smoketest -yarnInstall('test/smoke'); // node modules required for smoketest -yarnInstall('test/integration/browser'); // node modules required for integration -yarnInstallBuildDependencies(); // node modules for watching, specific to host node version, not electron - cp.execSync('git config pull.rebase true'); diff --git a/build/npm/update-localization-extension.js b/build/npm/update-localization-extension.js index 499ad918ec..001c4ee199 100644 --- a/build/npm/update-localization-extension.js +++ b/build/npm/update-localization-extension.js @@ -20,19 +20,12 @@ function update(options) { if (!idOrPath) { throw new Error('Argument must be the location of the localization extension.'); } - let transifex = options.transifex; let location = options.location; - if (transifex === true && location !== undefined) { - throw new Error('Either --transifex or --location can be specified, but not both.'); - } - if (!transifex && !location) { - transifex = true; - } if (location !== undefined && !fs.existsSync(location)) { throw new Error(`${location} doesn't exist.`); } let locExtFolder = idOrPath; - if (/^\w{2}(-\w+)?$/.test(idOrPath)) { + if (/^\w{2,3}(-\w+)?$/.test(idOrPath)) { locExtFolder = path.join('..', 'vscode-loc', 'i18n', `vscode-language-pack-${idOrPath}`); } let locExtStat = fs.statSync(locExtFolder); @@ -53,79 +46,55 @@ function update(options) { if (!localization.languageId || !localization.languageName || !localization.localizedLanguageName) { throw new Error('Each localization contribution must define "languageId", "languageName" and "localizedLanguageName" properties.'); } - let server = localization.server || 'www.transifex.com'; - let userName = localization.userName || 'api'; - let apiToken = process.env.TRANSIFEX_API_TOKEN; - let languageId = localization.transifexId || localization.languageId; + let languageId = localization.languageId; let translationDataFolder = path.join(locExtFolder, 'translations'); - if (languageId === "zh-cn") { - languageId = "zh-hans"; - } - if (languageId === "zh-tw") { - languageId = "zh-hant"; + + switch (languageId) { + case 'zh-cn': + languageId = 'zh-Hans'; + break; + case 'zh-tw': + languageId = 'zh-Hant'; + break; + case 'pt-br': + languageId = 'pt-BR'; + break; } + if (fs.existsSync(translationDataFolder) && fs.existsSync(path.join(translationDataFolder, 'main.i18n.json'))) { console.log('Clearing \'' + translationDataFolder + '\'...'); rimraf.sync(translationDataFolder); } - if (transifex) { - console.log(`Downloading translations for ${languageId} to '${translationDataFolder}' ...`); - let translationPaths = []; - i18n.pullI18nPackFiles(server, userName, apiToken, { id: languageId }, translationPaths) - .on('error', (error) => { - console.log(`Error occurred while importing translations:`); - translationPaths = undefined; - if (Array.isArray(error)) { - error.forEach(console.log); - } else if (error) { - console.log(error); - } else { - console.log('Unknown error'); + console.log(`Importing translations for ${languageId} form '${location}' to '${translationDataFolder}' ...`); + let translationPaths = []; + gulp.src(path.join(location, '**', languageId, '*.xlf'), { silent: false }) + .pipe(i18n.prepareI18nPackFiles(i18n.externalExtensionsWithTranslations, translationPaths, languageId === 'ps')) + .on('error', (error) => { + console.log(`Error occurred while importing translations:`); + translationPaths = undefined; + if (Array.isArray(error)) { + error.forEach(console.log); + } else if (error) { + console.log(error); + } else { + console.log('Unknown error'); + } + }) + .pipe(vfs.dest(translationDataFolder)) + .on('end', function () { + if (translationPaths !== undefined) { + localization.translations = []; + for (let tp of translationPaths) { + localization.translations.push({ id: tp.id, path: `./translations/${tp.resourceName}` }); } - }) - .pipe(vfs.dest(translationDataFolder)) - .on('end', function () { - if (translationPaths !== undefined) { - localization.translations = []; - for (let tp of translationPaths) { - localization.translations.push({ id: tp.id, path: `./translations/${tp.resourceName}`}); - } - fs.writeFileSync(path.join(locExtFolder, 'package.json'), JSON.stringify(packageJSON, null, '\t')); - } - }); - } else { - console.log(`Importing translations for ${languageId} form '${location}' to '${translationDataFolder}' ...`); - let translationPaths = []; - gulp.src(path.join(location, languageId, '**', '*.xlf')) - .pipe(i18n.prepareI18nPackFiles(i18n.externalExtensionsWithTranslations, translationPaths, languageId === 'ps')) - .on('error', (error) => { - console.log(`Error occurred while importing translations:`); - translationPaths = undefined; - if (Array.isArray(error)) { - error.forEach(console.log); - } else if (error) { - console.log(error); - } else { - console.log('Unknown error'); - } - }) - .pipe(vfs.dest(translationDataFolder)) - .on('end', function () { - if (translationPaths !== undefined) { - localization.translations = []; - for (let tp of translationPaths) { - localization.translations.push({ id: tp.id, path: `./translations/${tp.resourceName}`}); - } - fs.writeFileSync(path.join(locExtFolder, 'package.json'), JSON.stringify(packageJSON, null, '\t')); - } - }); - } + fs.writeFileSync(path.join(locExtFolder, 'package.json'), JSON.stringify(packageJSON, null, '\t') + '\n'); + } + }); }); } if (path.basename(process.argv[1]) === 'update-localization-extension.js') { var options = minimist(process.argv.slice(2), { - boolean: 'transifex', string: 'location' }); update(options); diff --git a/build/package.json b/build/package.json index 4e3013eb44..30ac3b7675 100644 --- a/build/package.json +++ b/build/package.json @@ -25,7 +25,7 @@ "@types/minimist": "^1.2.1", "@types/mkdirp": "^1.0.1", "@types/mocha": "^8.2.0", - "@types/node": "^14.14.37", + "@types/node": "14.x", "@types/p-limit": "^2.2.0", "@types/plist": "^3.0.2", "@types/pump": "^1.0.1", @@ -42,7 +42,7 @@ "byline": "^5.0.0", "colors": "^1.4.0", "electron-osx-sign": "^0.4.16", - "esbuild": "^0.8.30", + "esbuild": "^0.12.6", "fs-extra": "^9.1.0", "documentdb": "1.13.0", "got": "11.8.1", @@ -56,7 +56,7 @@ "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-node-resolve": "^5.2.0", "source-map": "0.6.1", - "typescript": "^4.3.0-dev.20210426", + "typescript": "^4.4.0-dev.20210607", "vsce": "1.48.0", "vscode-universal": "deepak1556/universal#61454d96223b774c53cda10f72c2098c0ce02d58" }, diff --git a/build/tsconfig.json b/build/tsconfig.json index 9ffce5e262..fac72cebf0 100644 --- a/build/tsconfig.json +++ b/build/tsconfig.json @@ -13,6 +13,8 @@ "allowJs": true, "checkJs": true, "strict": true, + "strictOptionalProperties": false, + "useUnknownInCatchVariables": false, "noUnusedLocals": true, "noUnusedParameters": true, "newLine": "lf" @@ -22,6 +24,6 @@ ], "exclude": [ "node_modules/**", - "actions/**" + "actions/**" // {{SQL CARBON EDIT}} ] } diff --git a/build/yarn.lock b/build/yarn.lock index 6291aa104b..f0b82c6e1b 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -383,16 +383,16 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.51.tgz#b31d716fb8d58eeb95c068a039b9b6292817d5fb" integrity sha512-El3+WJk2D/ppWNd2X05aiP5l2k4EwF7KwheknQZls+I26eSICoWRhRIJ56jGgw2dqNGQ5LtNajmBU2ajS28EvQ== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== + "@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/node@^14.14.37": - version "14.14.37" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.37.tgz#a3dd8da4eb84a996c36e331df98d82abd76b516e" - integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw== - "@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" @@ -1038,10 +1038,10 @@ entities@~2.1.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== -esbuild@^0.8.30: - version "0.8.47" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.8.47.tgz#5d5c59b7dcb8a20dfadf65a985e5e5ca7b24eff2" - integrity sha512-4C9pInguP36c9CRDMSb6W1KrMfXrLIQVtI02Vglc43RBV0Dw49ODEnP6985mrz5iqCdQ2Cxmb9i670J/s3DBPA== +esbuild@^0.12.6: + version "0.12.6" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.6.tgz#85bc755c7cf3005d4f34b4f10f98049ce0ee67ce" + integrity sha512-RDvVLvAjsq/kIZJoneMiUOH7EE7t2QaW7T3Q7EdQij14+bZbDq5sndb0tTanmHIFSqZVMBMMyqzVHkS3dJobeA== eslint-scope@^5.0.0: version "5.1.1" @@ -2001,10 +2001,10 @@ typescript@^4.1.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72" integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA== -typescript@^4.3.0-dev.20210426: - version "4.3.0-dev.20210426" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.0-dev.20210426.tgz#00198cb8828f6a04b4e0ae32554a486bf7137a53" - integrity sha512-8YTqlzf3w8O8XwnnRlwRV2rswu7V7WEPUnAnH1BPPMrr06thNByMjIadA5SDW3tUJc1MG8Uj3NgZYocU5fWTVg== +typescript@^4.4.0-dev.20210607: + version "4.4.0-dev.20210607" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.0-dev.20210607.tgz#ea802e420785ef3b6b9c2e12d1ff4b8d2e52ee19" + integrity sha512-tKAp1IL4APSdxD7xHLDU6tIDOEN8yJOTUGG+cSdLunmysl3yOkGrdUbByDaFDmGjKywghGhQvcG8gOqbLUcDcg== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" diff --git a/cglicenses.json b/cglicenses.json index 777f9affa7..7c42f531d5 100644 --- a/cglicenses.json +++ b/cglicenses.json @@ -19,7 +19,7 @@ ] }, { - // Reason: The license at https://github.com/Microsoft/TypeScript/blob/master/LICENSE.txt + // Reason: The license at https://github.com/microsoft/TypeScript/blob/master/LICENSE.txt // does not include a clear Copyright statement. "name": "typescript", "prependLicenseText": [ @@ -54,35 +54,6 @@ "Copyright (c) Microsoft Corporation. All rights reserved." ] }, - { - // Reason: The npm package lacks a repoURL field - // So the license at https://github.com/floatdrop/pinkie/blob/master/license - // cannot be found by the OSS tool automatically. - "name": "pinkie", - "fullLicenseText": [ - "The MIT License (MIT)", - "", - "Copyright (c) Vsevolod Strukchinsky (github.com/floatdrop)", - "", - "Permission is hereby granted, free of charge, to any person obtaining a copy", - "of this software and associated documentation files (the \"Software\"), to deal", - "in the Software without restriction, including without limitation the rights", - "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", - "copies of the Software, and to permit persons to whom the Software is", - "furnished to do so, subject to the following conditions:", - "", - "The above copyright notice and this permission notice shall be included in", - "all copies or substantial portions of the Software.", - "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", - "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", - "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", - "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", - "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", - "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN", - "THE SOFTWARE." - ] - }, { "name": "big-integer", "prependLicenseText": [ @@ -134,6 +105,7 @@ { // Reason: Repository lacks license text. // https://github.com/LinusU/load-yaml-file/blob/master/package.json declares MIT. + // https://github.com/LinusU/load-yaml-file/issues/2 "name": "load-yaml-file", "fullLicenseText": [ "MIT License", @@ -149,6 +121,7 @@ { // Reason: Repository lacks license text. // https://github.com/othiym23/emitter-listener/blob/master/package.json declares BSD-2-Clause. + // https://github.com/othiym23/emitter-listener/issues/3 "name": "emitter-listener", "fullLicenseText": [ "BSD 2-Clause \"Simplified\" License", @@ -174,19 +147,5 @@ "(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS", "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ] - }, - { - // Reason: Repository has been deleted (package.json declares MIT). - "name": "vscode-js-debug-cdp-proxy-api", - "fullLicenseText": [ - "MIT License", - "Copyright (c) Manuel Alabor", - "", - "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:", - "", - "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.", - "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." - ] } ] diff --git a/cgmanifest.json b/cgmanifest.json index 474d6191ba..22cbc50562 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -60,12 +60,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "8d55658bfa8b5983e1a90ad079c2e2ac91ee7af0" + "commitHash": "30f82dd1cb8140ccb5c6a4960eef8e3b8c15eeba" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "12.0.7" + "version": "12.0.9" }, { "component": { diff --git a/extensions/azuremonitor/src/index.ts b/extensions/azuremonitor/src/index.ts index 415fc08af0..2ae4bfeba6 100644 --- a/extensions/azuremonitor/src/index.ts +++ b/extensions/azuremonitor/src/index.ts @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. +// Licensed under the Source EULA. /* * This file imports all public APIs for this extension and used to generate index.d.ts. diff --git a/extensions/bat/cgmanifest.json b/extensions/bat/cgmanifest.json index 5bc3e285f0..7e0426c138 100644 --- a/extensions/bat/cgmanifest.json +++ b/extensions/bat/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "mmims/language-batchfile", "repositoryUrl": "https://github.com/mmims/language-batchfile", - "commitHash": "95ea8c699f7a8296b15767069868532d52631c46" + "commitHash": "6154ae25a24e01ac9329e7bcf958e093cd8733a9" } }, "license": "MIT", - "version": "0.7.5" + "version": "0.7.6" } ], "version": 1 diff --git a/extensions/bat/syntaxes/batchfile.tmLanguage.json b/extensions/bat/syntaxes/batchfile.tmLanguage.json index 9eff3c9de2..85e03dbc85 100644 --- a/extensions/bat/syntaxes/batchfile.tmLanguage.json +++ b/extensions/bat/syntaxes/batchfile.tmLanguage.json @@ -4,9 +4,18 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/mmims/language-batchfile/commit/95ea8c699f7a8296b15767069868532d52631c46", + "version": "https://github.com/mmims/language-batchfile/commit/6154ae25a24e01ac9329e7bcf958e093cd8733a9", "name": "Batch File", "scopeName": "source.batchfile", + "injections": { + "L:meta.block.repeat.batchfile": { + "patterns": [ + { + "include": "#repeatParameter" + } + ] + } + }, "patterns": [ { "include": "#commands" @@ -46,7 +55,7 @@ "commands": { "patterns": [ { - "match": "(?<=^|[\\s@])(?i:adprep|append|arp|assoc|at|atmadm|attrib|auditpol|autochk|autoconv|autofmt|bcdboot|bcdedit|bdehdcfg|bitsadmin|bootcfg|brea|cacls|cd|certreq|certutil|change|chcp|chdir|chglogon|chgport|chgusr|chkdsk|chkntfs|choice|cipher|clip|cls|clscluadmin|cluster|cmd|cmdkey|cmstp|color|comp|compact|convert|copy|cprofile|cscript|csvde|date|dcdiag|dcgpofix|dcpromo|defra|del|dfscmd|dfsdiag|dfsrmig|diantz|dir|dirquota|diskcomp|diskcopy|diskpart|diskperf|diskraid|diskshadow|dispdiag|doin|dnscmd|doskey|driverquery|dsacls|dsadd|dsamain|dsdbutil|dsget|dsmgmt|dsmod|dsmove|dsquery|dsrm|edit|endlocal|eraseesentutl|eventcreate|eventquery|eventtriggers|evntcmd|expand|extract|fc|filescrn|find|findstr|finger|flattemp|fonde|forfiles|format|freedisk|fsutil|ftp|ftype|fveupdate|getmac|gettype|gpfixup|gpresult|gpupdate|graftabl|hashgen|hep|helpctr|hostname|icacls|iisreset|inuse|ipconfig|ipxroute|irftp|ismserv|jetpack|klist|ksetup|ktmutil|ktpass|label|ldifd|ldp|lodctr|logman|logoff|lpq|lpr|macfile|makecab|manage-bde|mapadmin|md|mkdir|mklink|mmc|mode|more|mount|mountvol|move|mqbup|mqsvc|mqtgsvc|msdt|msg|msiexec|msinfo32|mstsc|nbtstat|net computer|net group|net localgroup|net print|net session|net share|net start|net stop|net use|net user|net view|net|netcfg|netdiag|netdom|netsh|netstat|nfsadmin|nfsshare|nfsstat|nlb|nlbmgr|nltest|nslookup|ntackup|ntcmdprompt|ntdsutil|ntfrsutl|openfiles|pagefileconfig|path|pathping|pause|pbadmin|pentnt|perfmon|ping|pnpunatten|pnputil|popd|powercfg|powershell|powershell_ise|print|prncnfg|prndrvr|prnjobs|prnmngr|prnport|prnqctl|prompt|pubprn|pushd|pushprinterconnections|pwlauncher|qappsrv|qprocess|query|quser|qwinsta|rasdial|rcp|rd|rdpsign|regentc|recover|redircmp|redirusr|reg|regini|regsvr32|relog|ren|rename|rendom|repadmin|repair-bde|replace|reset session|rxec|risetup|rmdir|robocopy|route|rpcinfo|rpcping|rsh|runas|rundll32|rwinsta|scp|sc|schtasks|scwcmd|secedit|serverceipoptin|servrmanagercmd|serverweroptin|setspn|setx|sfc|shadow|shift|showmount|shutdown|sort|ssh|start|storrept|subst|sxstrace|ysocmgr|systeminfo|takeown|tapicfg|taskkill|tasklist|tcmsetup|telnet|tftp|time|timeout|title|tlntadmn|tpmvscmgr|tpmvscmgr|tacerpt|tracert|tree|tscon|tsdiscon|tsecimp|tskill|tsprof|type|typeperf|tzutil|uddiconfig|umount|unlodctr|ver|verifier|verif|vol|vssadmin|w32tm|waitfor|wbadmin|wdsutil|wecutil|wevtutil|where|whoami|winnt|winnt32|winpop|winrm|winrs|winsat|wlbs|mic|wscript|xcopy)(?=$|\\s)", + "match": "(?<=^|[\\s@])(?i:adprep|append|arp|assoc|at|atmadm|attrib|auditpol|autochk|autoconv|autofmt|bcdboot|bcdedit|bdehdcfg|bitsadmin|bootcfg|brea|cacls|cd|certreq|certutil|change|chcp|chdir|chglogon|chgport|chgusr|chkdsk|chkntfs|choice|cipher|clip|cls|clscluadmin|cluster|cmd|cmdkey|cmstp|color|comp|compact|convert|copy|cprofile|cscript|csvde|date|dcdiag|dcgpofix|dcpromo|defra|del|dfscmd|dfsdiag|dfsrmig|diantz|dir|dirquota|diskcomp|diskcopy|diskpart|diskperf|diskraid|diskshadow|dispdiag|doin|dnscmd|doskey|driverquery|dsacls|dsadd|dsamain|dsdbutil|dsget|dsmgmt|dsmod|dsmove|dsquery|dsrm|edit|endlocal|eraseesentutl|eventcreate|eventquery|eventtriggers|evntcmd|expand|extract|fc|filescrn|find|findstr|finger|flattemp|fonde|forfiles|format|freedisk|fsutil|ftp|ftype|fveupdate|getmac|gettype|gpfixup|gpresult|gpupdate|graftabl|hashgen|hep|helpctr|hostname|icacls|iisreset|inuse|ipconfig|ipxroute|irftp|ismserv|jetpack|klist|ksetup|ktmutil|ktpass|label|ldifd|ldp|lodctr|logman|logoff|lpq|lpr|macfile|makecab|manage-bde|mapadmin|md|mkdir|mklink|mmc|mode|more|mount|mountvol|move|mqbup|mqsvc|mqtgsvc|msdt|msg|msiexec|msinfo32|mstsc|nbtstat|net computer|net group|net localgroup|net print|net session|net share|net start|net stop|net use|net user|net view|net|netcfg|netdiag|netdom|netsh|netstat|nfsadmin|nfsshare|nfsstat|nlb|nlbmgr|nltest|nslookup|ntackup|ntcmdprompt|ntdsutil|ntfrsutl|openfiles|pagefileconfig|path|pathping|pause|pbadmin|pentnt|perfmon|ping|pnpunatten|pnputil|popd|powercfg|powershell|powershell_ise|print|prncnfg|prndrvr|prnjobs|prnmngr|prnport|prnqctl|prompt|pubprn|pushd|pushprinterconnections|pwlauncher|qappsrv|qprocess|query|quser|qwinsta|rasdial|rcp|rd|rdpsign|regentc|recover|redircmp|redirusr|reg|regini|regsvr32|relog|ren|rename|rendom|repadmin|repair-bde|replace|reset session|rxec|risetup|rmdir|robocopy|route|rpcinfo|rpcping|rsh|runas|rundll32|rwinsta|sc|schtasks|scp|scwcmd|secedit|serverceipoptin|servrmanagercmd|serverweroptin|setspn|setx|sfc|sftp|shadow|shift|showmount|shutdown|sort|ssh|ssh-add|ssh-agent|ssh-keygen|ssh-keyscan|start|storrept|subst|sxstrace|ysocmgr|systeminfo|takeown|tapicfg|taskkill|tasklist|tcmsetup|telnet|tftp|time|timeout|title|tlntadmn|tpmvscmgr|tpmvscmgr|tacerpt|tracert|tree|tscon|tsdiscon|tsecimp|tskill|tsprof|type|typeperf|tzutil|uddiconfig|umount|unlodctr|ver|verifier|verif|vol|vssadmin|w32tm|waitfor|wbadmin|wdsutil|wecutil|wevtutil|where|whoami|winnt|winnt32|winpop|winrm|winrs|winsat|wlbs|wmic|wscript|wsl|xcopy)(?=$|\\s)", "name": "keyword.command.batchfile" }, { @@ -285,7 +294,7 @@ "name": "keyword.operator.logical.batchfile" }, { - "match": "([^ ][^=]*)(=)", + "match": "([^ =]*)(=)", "captures": { "1": { "name": "variable.other.readwrite.batchfile" @@ -420,8 +429,38 @@ "name": "keyword.control.conditional.batchfile" }, { - "match": "(?<=^|\\s)(?i)for(?=\\s)", - "name": "keyword.control.repeat.batchfile" + "begin": "(?<=^|[\\s(&^])(?i)for(?=\\s)", + "beginCaptures": { + "0": { + "name": "keyword.control.repeat.batchfile" + } + }, + "name": "meta.block.repeat.batchfile", + "end": "\\n", + "patterns": [ + { + "begin": "(?<=[\\s^])(?i)in(?=\\s)", + "beginCaptures": { + "0": { + "name": "keyword.control.repeat.in.batchfile" + } + }, + "end": "(?<=[\\s)^])(?i)do(?=\\s)|\\n", + "endCaptures": { + "0": { + "name": "keyword.control.repeat.do.batchfile" + } + }, + "patterns": [ + { + "include": "$self" + } + ] + }, + { + "include": "$self" + } + ] } ] }, @@ -436,7 +475,7 @@ "labels": { "patterns": [ { - "match": "(?i)(?:^\\s*|(?<=goto)\\s*)(:)([^+=,;:\\s].*)$", + "match": "(?i)(?:^\\s*|(?<=call|goto)\\s*)(:)([^+=,;:\\s]\\S*)", "captures": { "1": { "name": "punctuation.separator.batchfile" @@ -512,6 +551,19 @@ } ] }, + "repeatParameter": { + "patterns": [ + { + "match": "(%%)(?:(?i:~[fdpnxsatz]*(?:\\$PATH:)?)?[a-zA-Z])", + "captures": { + "1": { + "name": "punctuation.definition.variable.batchfile" + } + }, + "name": "variable.parameter.repeat.batchfile" + } + ] + }, "strings": { "patterns": [ { @@ -546,15 +598,13 @@ "variables": { "patterns": [ { - "match": "(%)((~([fdpnxsatz]|\\$PATH:)*)?\\d|\\*)", + "match": "(%)(?:(?i:~[fdpnxsatz]*(?:\\$PATH:)?)?\\d|\\*)", "captures": { "1": { "name": "punctuation.definition.variable.batchfile" - }, - "2": { - "name": "variable.parameter.batchfile" } - } + }, + "name": "variable.parameter.batchfile" }, { "include": "#variable" diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index 324e0c8060..80e627e5f9 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -139,7 +139,7 @@ ] }, "devDependencies": { - "@types/node": "^12.19.9" + "@types/node": "14.x" }, "repository": { "type": "git", diff --git a/extensions/configuration-editing/schemas/attachContainer.schema.json b/extensions/configuration-editing/schemas/attachContainer.schema.json index ec765b8fc2..9cdd3a5f6d 100644 --- a/extensions/configuration-editing/schemas/attachContainer.schema.json +++ b/extensions/configuration-editing/schemas/attachContainer.schema.json @@ -54,6 +54,19 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "default": { @@ -80,7 +93,13 @@ "properties": { "onAutoForward": { "type": "string", - "enum": ["notify", "openBrowser", "openPreview", "silent", "ignore"], + "enum": [ + "notify", + "openBrowser", + "openPreview", + "silent", + "ignore" + ], "enumDescriptions": [ "Shows a notification when a port is automatically forwarded.", "Opens the browser when the port is automatically forwarded. Depending on your settings, this could open an embedded browser.", @@ -100,9 +119,28 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, - "defaultSnippets": [{ "body": { "onAutoForward": "ignore" } }], + "defaultSnippets": [ + { + "body": { + "onAutoForward": "ignore" + } + } + ], "markdownDescription": "Set default properties that are applied to all ports that don't get properties from the setting `remote.portsAttributes`. For example:\n\n```\n{\n \"onAutoForward\": \"ignore\"\n}\n```", "additionalProperties": false }, diff --git a/extensions/configuration-editing/schemas/devContainer.schema.generated.json b/extensions/configuration-editing/schemas/devContainer.schema.generated.json index 90e70c0125..112b4e4827 100644 --- a/extensions/configuration-editing/schemas/devContainer.schema.generated.json +++ b/extensions/configuration-editing/schemas/devContainer.schema.generated.json @@ -162,6 +162,19 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "default": { @@ -215,6 +228,19 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "defaultSnippets": [ @@ -246,7 +272,27 @@ "string", "array" ], - "description": "A command to run locally before anything else. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run locally before anything else. This command is run before \"onCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "onCreateCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container. This command is run after \"initializeCommand\" and before \"updateContentCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "updateContentCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container and rerun when the workspace content was updated while creating the container. This command is run after \"onCreateCommand\" and before \"postCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -256,7 +302,7 @@ "string", "array" ], - "description": "A command to run after creating the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after creating the container. This command is run after \"updateContentCommand\" and before \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -266,7 +312,7 @@ "string", "array" ], - "description": "A command to run after starting the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after starting the container. This command is run after \"postCreateCommand\" and before \"postAttachCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -276,11 +322,23 @@ "string", "array" ], - "description": "A command to run after attaching to the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run when attaching to the container. This command is run after \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } }, + "waitFor": { + "type": "string", + "enum": [ + "initializeCommand", + "onCreateCommand", + "updateContentCommand", + "postCreateCommand", + "postStartCommand", + "postAttachCommand" + ], + "description": "The user command to wait for before continuing execution in the background while the UI is starting up. The default is \"updateContentCommand\"." + }, "devPort": { "type": "integer", "description": "The port VS Code can use to connect to its backend." @@ -299,6 +357,28 @@ "type": "object", "additionalProperties": true, "description": "Codespaces-specific configuration." + }, + "hostRequirements": { + "type": "object", + "description": "Host hardware requirements.", + "properties": { + "cpus": { + "type": "integer", + "minimum": 1, + "description": "Number of required CPUs." + }, + "memory": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required RAM in bytes. Supports units tb, gb, mb and kb." + }, + "storage": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required disk space in bytes. Supports units tb, gb, mb and kb." + } + }, + "additionalProperties": false } }, "required": [ @@ -461,6 +541,19 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "default": { @@ -514,6 +607,19 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "defaultSnippets": [ @@ -545,7 +651,27 @@ "string", "array" ], - "description": "A command to run locally before anything else. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run locally before anything else. This command is run before \"onCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "onCreateCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container. This command is run after \"initializeCommand\" and before \"updateContentCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "updateContentCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container and rerun when the workspace content was updated while creating the container. This command is run after \"onCreateCommand\" and before \"postCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -555,7 +681,7 @@ "string", "array" ], - "description": "A command to run after creating the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after creating the container. This command is run after \"updateContentCommand\" and before \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -565,7 +691,7 @@ "string", "array" ], - "description": "A command to run after starting the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after starting the container. This command is run after \"postCreateCommand\" and before \"postAttachCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -575,11 +701,23 @@ "string", "array" ], - "description": "A command to run after attaching to the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run when attaching to the container. This command is run after \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } }, + "waitFor": { + "type": "string", + "enum": [ + "initializeCommand", + "onCreateCommand", + "updateContentCommand", + "postCreateCommand", + "postStartCommand", + "postAttachCommand" + ], + "description": "The user command to wait for before continuing execution in the background while the UI is starting up. The default is \"updateContentCommand\"." + }, "devPort": { "type": "integer", "description": "The port VS Code can use to connect to its backend." @@ -598,6 +736,28 @@ "type": "object", "additionalProperties": true, "description": "Codespaces-specific configuration." + }, + "hostRequirements": { + "type": "object", + "description": "Host hardware requirements.", + "properties": { + "cpus": { + "type": "integer", + "minimum": 1, + "description": "Number of required CPUs." + }, + "memory": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required RAM in bytes. Supports units tb, gb, mb and kb." + }, + "storage": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required disk space in bytes. Supports units tb, gb, mb and kb." + } + }, + "additionalProperties": false } }, "required": [ @@ -736,6 +896,19 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "default": { @@ -789,6 +962,19 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "defaultSnippets": [ @@ -820,7 +1006,27 @@ "string", "array" ], - "description": "A command to run locally before anything else. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run locally before anything else. This command is run before \"onCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "onCreateCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container. This command is run after \"initializeCommand\" and before \"updateContentCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "updateContentCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container and rerun when the workspace content was updated while creating the container. This command is run after \"onCreateCommand\" and before \"postCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -830,7 +1036,7 @@ "string", "array" ], - "description": "A command to run after creating the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after creating the container. This command is run after \"updateContentCommand\" and before \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -840,7 +1046,7 @@ "string", "array" ], - "description": "A command to run after starting the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after starting the container. This command is run after \"postCreateCommand\" and before \"postAttachCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -850,11 +1056,23 @@ "string", "array" ], - "description": "A command to run after attaching to the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run when attaching to the container. This command is run after \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } }, + "waitFor": { + "type": "string", + "enum": [ + "initializeCommand", + "onCreateCommand", + "updateContentCommand", + "postCreateCommand", + "postStartCommand", + "postAttachCommand" + ], + "description": "The user command to wait for before continuing execution in the background while the UI is starting up. The default is \"updateContentCommand\"." + }, "devPort": { "type": "integer", "description": "The port VS Code can use to connect to its backend." @@ -873,6 +1091,28 @@ "type": "object", "additionalProperties": true, "description": "Codespaces-specific configuration." + }, + "hostRequirements": { + "type": "object", + "description": "Host hardware requirements.", + "properties": { + "cpus": { + "type": "integer", + "minimum": 1, + "description": "Number of required CPUs." + }, + "memory": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required RAM in bytes. Supports units tb, gb, mb and kb." + }, + "storage": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required disk space in bytes. Supports units tb, gb, mb and kb." + } + }, + "additionalProperties": false } }, "required": [ @@ -977,6 +1217,19 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "default": { @@ -1030,6 +1283,19 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "defaultSnippets": [ @@ -1061,7 +1327,27 @@ "string", "array" ], - "description": "A command to run locally before anything else. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run locally before anything else. This command is run before \"onCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "onCreateCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container. This command is run after \"initializeCommand\" and before \"updateContentCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "updateContentCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container and rerun when the workspace content was updated while creating the container. This command is run after \"onCreateCommand\" and before \"postCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -1071,7 +1357,7 @@ "string", "array" ], - "description": "A command to run after creating the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after creating the container. This command is run after \"updateContentCommand\" and before \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -1081,7 +1367,7 @@ "string", "array" ], - "description": "A command to run after starting the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after starting the container. This command is run after \"postCreateCommand\" and before \"postAttachCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -1091,11 +1377,23 @@ "string", "array" ], - "description": "A command to run after attaching to the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run when attaching to the container. This command is run after \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } }, + "waitFor": { + "type": "string", + "enum": [ + "initializeCommand", + "onCreateCommand", + "updateContentCommand", + "postCreateCommand", + "postStartCommand", + "postAttachCommand" + ], + "description": "The user command to wait for before continuing execution in the background while the UI is starting up. The default is \"updateContentCommand\"." + }, "devPort": { "type": "integer", "description": "The port VS Code can use to connect to its backend." @@ -1114,6 +1412,28 @@ "type": "object", "additionalProperties": true, "description": "Codespaces-specific configuration." + }, + "hostRequirements": { + "type": "object", + "description": "Host hardware requirements.", + "properties": { + "cpus": { + "type": "integer", + "minimum": 1, + "description": "Number of required CPUs." + }, + "memory": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required RAM in bytes. Supports units tb, gb, mb and kb." + }, + "storage": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required disk space in bytes. Supports units tb, gb, mb and kb." + } + }, + "additionalProperties": false } }, "required": [ @@ -1187,6 +1507,19 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "default": { @@ -1240,6 +1573,19 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "defaultSnippets": [ @@ -1271,7 +1617,27 @@ "string", "array" ], - "description": "A command to run locally before anything else. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run locally before anything else. This command is run before \"onCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "onCreateCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container. This command is run after \"initializeCommand\" and before \"updateContentCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "updateContentCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container and rerun when the workspace content was updated while creating the container. This command is run after \"onCreateCommand\" and before \"postCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -1281,7 +1647,7 @@ "string", "array" ], - "description": "A command to run after creating the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after creating the container. This command is run after \"updateContentCommand\" and before \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -1291,7 +1657,7 @@ "string", "array" ], - "description": "A command to run after starting the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after starting the container. This command is run after \"postCreateCommand\" and before \"postAttachCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -1301,11 +1667,23 @@ "string", "array" ], - "description": "A command to run after attaching to the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run when attaching to the container. This command is run after \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } }, + "waitFor": { + "type": "string", + "enum": [ + "initializeCommand", + "onCreateCommand", + "updateContentCommand", + "postCreateCommand", + "postStartCommand", + "postAttachCommand" + ], + "description": "The user command to wait for before continuing execution in the background while the UI is starting up. The default is \"updateContentCommand\"." + }, "devPort": { "type": "integer", "description": "The port VS Code can use to connect to its backend." @@ -1324,6 +1702,28 @@ "type": "object", "additionalProperties": true, "description": "Codespaces-specific configuration." + }, + "hostRequirements": { + "type": "object", + "description": "Host hardware requirements.", + "properties": { + "cpus": { + "type": "integer", + "minimum": 1, + "description": "Number of required CPUs." + }, + "memory": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required RAM in bytes. Supports units tb, gb, mb and kb." + }, + "storage": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required disk space in bytes. Supports units tb, gb, mb and kb." + } + }, + "additionalProperties": false } }, "additionalProperties": false diff --git a/extensions/configuration-editing/schemas/devContainer.schema.src.json b/extensions/configuration-editing/schemas/devContainer.schema.src.json index 2987aad16c..14c13a958b 100644 --- a/extensions/configuration-editing/schemas/devContainer.schema.src.json +++ b/extensions/configuration-editing/schemas/devContainer.schema.src.json @@ -68,6 +68,16 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": ["http", "https"], + "description": "The protocol to use when forwarding this port." } }, "default": { @@ -120,6 +130,16 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": ["http", "https"], + "description": "The protocol to use when forwarding this port." } }, "defaultSnippets": [ @@ -151,7 +171,27 @@ "string", "array" ], - "description": "A command to run locally before anything else. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run locally before anything else. This command is run before \"onCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "onCreateCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container. This command is run after \"initializeCommand\" and before \"updateContentCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "updateContentCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container and rerun when the workspace content was updated while creating the container. This command is run after \"onCreateCommand\" and before \"postCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -161,7 +201,7 @@ "string", "array" ], - "description": "A command to run after creating the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after creating the container. This command is run after \"updateContentCommand\" and before \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -171,7 +211,7 @@ "string", "array" ], - "description": "A command to run after starting the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after starting the container. This command is run after \"postCreateCommand\" and before \"postAttachCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -181,11 +221,23 @@ "string", "array" ], - "description": "A command to run after attaching to the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run when attaching to the container. This command is run after \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } }, + "waitFor": { + "type": "string", + "enum": [ + "initializeCommand", + "onCreateCommand", + "updateContentCommand", + "postCreateCommand", + "postStartCommand", + "postAttachCommand" + ], + "description": "The user command to wait for before continuing execution in the background while the UI is starting up. The default is \"updateContentCommand\"." + }, "devPort": { "type": "integer", "description": "The port VS Code can use to connect to its backend." @@ -204,6 +256,32 @@ "type": "object", "additionalProperties": true, "description": "Codespaces-specific configuration." + }, + "hostRequirements": { + "type": "object", + "description": "Host hardware requirements.", + "allOf": [ + { + "type": "object", + "properties": { + "cpus": { + "type": "integer", + "minimum": 1, + "description": "Number of required CPUs." + }, + "memory": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required RAM in bytes. Supports units tb, gb, mb and kb." + }, + "storage": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required disk space in bytes. Supports units tb, gb, mb and kb." + } + } + } + ] } } }, diff --git a/extensions/configuration-editing/src/settingsDocumentHelper.ts b/extensions/configuration-editing/src/settingsDocumentHelper.ts index b82d5a662c..f788d11b6d 100644 --- a/extensions/configuration-editing/src/settingsDocumentHelper.ts +++ b/extensions/configuration-editing/src/settingsDocumentHelper.ts @@ -6,7 +6,7 @@ import * as vscode from 'vscode'; import { getLocation, Location, parse } from 'jsonc-parser'; import * as nls from 'vscode-nls'; -import { provideInstalledExtensionProposals, provideWorkspaceTrustExtensionProposals } from './extensionsProposals'; +import { provideInstalledExtensionProposals } from './extensionsProposals'; const localize = nls.loadMessageBundle(); @@ -60,13 +60,9 @@ export class SettingsDocument { return provideInstalledExtensionProposals(alreadyConfigured, `: [\n\t"ui"\n]`, range, true); } - // extensions.supportUntrustedWorkspaces - if (location.path[0] === 'extensions.supportUntrustedWorkspaces' && location.path.length === 2 && location.isAtPropertyKey) { - let alreadyConfigured: string[] = []; - try { - alreadyConfigured = Object.keys(parse(this.document.getText())['extensions.supportUntrustedWorkspaces']); - } catch (e) {/* ignore error */ } - return provideWorkspaceTrustExtensionProposals(alreadyConfigured, range); + // remote.portsAttributes + if (location.path[0] === 'remote.portsAttributes' && location.path.length === 2 && location.isAtPropertyKey) { + return this.providePortsAttributesCompletionItem(range); } return this.provideLanguageOverridesCompletionItems(location, position); @@ -247,6 +243,31 @@ export class SettingsDocument { return Promise.resolve([]); } + private providePortsAttributesCompletionItem(range: vscode.Range): vscode.CompletionItem[] { + return [this.newSnippetCompletionItem( + { + label: '\"3000\"', + documentation: 'Single Port Attribute', + range, + snippet: '\n \"${1:3000}\": {\n \"label\": \"${2:Application}\",\n \"onAutoForward\": \"${3:openPreview}\"\n }\n' + }), + this.newSnippetCompletionItem( + { + label: '\"5000-6000\"', + documentation: 'Ranged Port Attribute', + range, + snippet: '\n \"${1:40000-55000}\": {\n \"onAutoForward\": \"${2:ignore}\"\n }\n' + }), + this.newSnippetCompletionItem( + { + label: '\".+\\\\/server.js\"', + documentation: 'Command Match Port Attribute', + range, + snippet: '\n \"${1:.+\\\\/server.js\}\": {\n \"label\": \"${2:Application}\",\n \"onAutoForward\": \"${3:openPreview}\"\n }\n' + }) + ]; + } + private newSimpleCompletionItem(text: string, range: vscode.Range, description?: string, insertText?: string): vscode.CompletionItem { const item = new vscode.CompletionItem(text); item.kind = vscode.CompletionItemKind.Value; diff --git a/extensions/configuration-editing/yarn.lock b/extensions/configuration-editing/yarn.lock index ebfaa046da..d7215349a9 100644 --- a/extensions/configuration-editing/yarn.lock +++ b/extensions/configuration-editing/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@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@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== jsonc-parser@^2.2.1: version "2.2.1" diff --git a/extensions/dart/.vscodeignore b/extensions/dart/.vscodeignore new file mode 100644 index 0000000000..d9011becfb --- /dev/null +++ b/extensions/dart/.vscodeignore @@ -0,0 +1,2 @@ +build/** +cgmanifest.json diff --git a/extensions/dart/cgmanifest.json b/extensions/dart/cgmanifest.json new file mode 100644 index 0000000000..52c4a994d1 --- /dev/null +++ b/extensions/dart/cgmanifest.json @@ -0,0 +1,46 @@ +{ + "registrations": [ + { + "component": { + "type": "git", + "git": { + "name": "dart-lang/dart-syntax-highlight", + "repositoryUrl": "https://github.com/dart-lang/dart-syntax-highlight", + "commitHash": "65f211722c85e9fdf0aa658d5663e6ccaf2ebe25" + } + }, + "licenseDetail": [ + "Copyright 2020, the Dart project authors.", + "", + "Redistribution and use in source and binary forms, with or without", + "modification, are permitted provided that the following conditions are", + "met:", + "", + " * Redistributions of source code must retain the above copyright", + " notice, this list of conditions and the following disclaimer.", + " * Redistributions in binary form must reproduce the above", + " copyright notice, this list of conditions and the following", + " disclaimer in the documentation and/or other materials provided", + " with the distribution.", + " * Neither the name of Google LLC nor the names of its", + " contributors may be used to endorse or promote products derived", + " from this software without specific prior written permission.", + "", + "THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS", + "\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT", + "LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR", + "A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT", + "OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,", + "SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT", + "LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,", + "DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY", + "THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT", + "(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE", + "OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + ], + "license": "BSD", + "version": "0.0.0" + } + ], + "version": 1 +} diff --git a/extensions/dart/language-configuration.json b/extensions/dart/language-configuration.json new file mode 100644 index 0000000000..9d44a40ee8 --- /dev/null +++ b/extensions/dart/language-configuration.json @@ -0,0 +1,29 @@ +{ + "comments": { + "lineComment": "//", + "blockComment": [ "/*", "*/" ] + }, + "brackets": [ + ["{", "}"], + ["[", "]"], + ["(", ")"] + ], + "autoClosingPairs": [ + { "open": "{", "close": "}" }, + { "open": "[", "close": "]" }, + { "open": "(", "close": ")" }, + { "open": "'", "close": "'", "notIn": ["string", "comment"] }, + { "open": "\"", "close": "\"", "notIn": ["string"] }, + { "open": "`", "close": "`", "notIn": ["string", "comment"] }, + { "open": "/**", "close": " */", "notIn": ["string"] } + ], + "surroundingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["<", ">"], + ["'", "'"], + ["\"", "\""], + ["`", "`"] + ] +} diff --git a/extensions/dart/package.json b/extensions/dart/package.json new file mode 100644 index 0000000000..92863ffb2b --- /dev/null +++ b/extensions/dart/package.json @@ -0,0 +1,35 @@ +{ + "name": "dart", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "0.10.x" + }, + "scripts": { + "update-grammar": "node ../node_modules/vscode-grammar-updater/bin dart-lang/dart-syntax-highlight grammars/dart.json ./syntaxes/dart.tmLanguage.json" + }, + "contributes": { + "languages": [ + { + "id": "dart", + "extensions": [ + ".dart" + ], + "aliases": [ + "Dart" + ], + "configuration": "./language-configuration.json" + } + ], + "grammars": [ + { + "language": "dart", + "scopeName": "source.dart", + "path": "./syntaxes/dart.tmLanguage.json" + } + ] + } +} diff --git a/extensions/dart/package.nls.json b/extensions/dart/package.nls.json new file mode 100644 index 0000000000..71e6b91e93 --- /dev/null +++ b/extensions/dart/package.nls.json @@ -0,0 +1,4 @@ +{ + "displayName": "Dart Language Basics", + "description": "Provides syntax highlighting & bracket matching in Dart files." +} diff --git a/extensions/dart/syntaxes/dart.tmLanguage.json b/extensions/dart/syntaxes/dart.tmLanguage.json new file mode 100644 index 0000000000..fc2fae5bc4 --- /dev/null +++ b/extensions/dart/syntaxes/dart.tmLanguage.json @@ -0,0 +1,439 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/dart-lang/dart-syntax-highlight/blob/master/grammars/dart.json", + "If you want to provide a fix or improvement, please create a pull request against the original repository.", + "Once accepted there, we are happy to receive an update request." + ], + "version": "https://github.com/dart-lang/dart-syntax-highlight/commit/65f211722c85e9fdf0aa658d5663e6ccaf2ebe25", + "name": "Dart", + "scopeName": "source.dart", + "patterns": [ + { + "name": "meta.preprocessor.script.dart", + "match": "^(#!.*)$" + }, + { + "name": "meta.declaration.dart", + "begin": "^\\w*\\b(library|import|part of|part|export)\\b", + "beginCaptures": { + "0": { + "name": "keyword.other.import.dart" + } + }, + "end": ";", + "endCaptures": { + "0": { + "name": "punctuation.terminator.dart" + } + }, + "patterns": [ + { + "include": "#strings" + }, + { + "include": "#comments" + }, + { + "name": "keyword.other.import.dart", + "match": "\\b(as|show|hide)\\b" + } + ] + }, + { + "include": "#comments" + }, + { + "include": "#punctuation" + }, + { + "include": "#annotations" + }, + { + "include": "#keywords" + }, + { + "include": "#constants-and-special-vars" + }, + { + "include": "#strings" + } + ], + "repository": { + "dartdoc": { + "patterns": [ + { + "match": "(\\[.*?\\])", + "captures": { + "0": { + "name": "variable.name.source.dart" + } + } + }, + { + "match": "^ {4,}(?![ \\*]).*", + "captures": { + "0": { + "name": "variable.name.source.dart" + } + } + }, + { + "contentName": "variable.other.source.dart", + "begin": "```.*?$", + "end": "```" + }, + { + "match": "(`.*?`)", + "captures": { + "0": { + "name": "variable.other.source.dart" + } + } + }, + { + "match": "(`.*?`)", + "captures": { + "0": { + "name": "variable.other.source.dart" + } + } + }, + { + "match": "(\\* (( ).*))$", + "captures": { + "2": { + "name": "variable.other.source.dart" + } + } + }, + { + "match": "(\\* .*)$" + } + ] + }, + "comments": { + "patterns": [ + { + "name": "comment.block.empty.dart", + "match": "/\\*\\*/", + "captures": { + "0": { + "name": "punctuation.definition.comment.dart" + } + } + }, + { + "include": "#comments-doc-oldschool" + }, + { + "include": "#comments-doc" + }, + { + "include": "#comments-inline" + } + ] + }, + "comments-doc-oldschool": { + "patterns": [ + { + "name": "comment.block.documentation.dart", + "begin": "/\\*\\*", + "end": "\\*/", + "patterns": [ + { + "include": "#comments-doc-oldschool" + }, + { + "include": "#comments-block" + }, + { + "include": "#dartdoc" + } + ] + } + ] + }, + "comments-doc": { + "patterns": [ + { + "name": "comment.block.documentation.dart", + "begin": "///", + "while": "^\\s*///", + "patterns": [ + { + "include": "#dartdoc" + } + ] + } + ] + }, + "comments-inline": { + "patterns": [ + { + "include": "#comments-block" + }, + { + "match": "((//).*)$", + "captures": { + "1": { + "name": "comment.line.double-slash.dart" + } + } + } + ] + }, + "comments-block": { + "patterns": [ + { + "name": "comment.block.dart", + "begin": "/\\*", + "end": "\\*/", + "patterns": [ + { + "include": "#comments-block" + } + ] + } + ] + }, + "annotations": { + "patterns": [ + { + "name": "storage.type.annotation.dart", + "match": "@[a-zA-Z]+" + } + ] + }, + "constants-and-special-vars": { + "patterns": [ + { + "name": "constant.language.dart", + "match": "(?)", + "captures": { + "1": { + "name": "entity.name.function.dart" + } + } + } + ] + }, + "keywords": { + "patterns": [ + { + "name": "keyword.cast.dart", + "match": "(?>>?|~|\\^|\\||&)" + }, + { + "name": "keyword.operator.assignment.bitwise.dart", + "match": "((&|\\^|\\||<<|>>>?)=)" + }, + { + "name": "keyword.operator.closure.dart", + "match": "(=>)" + }, + { + "name": "keyword.operator.comparison.dart", + "match": "(==|!=|<=?|>=?)" + }, + { + "name": "keyword.operator.assignment.arithmetic.dart", + "match": "(([+*/%-]|\\~)=)" + }, + { + "name": "keyword.operator.assignment.dart", + "match": "(=)" + }, + { + "name": "keyword.operator.increment-decrement.dart", + "match": "(\\-\\-|\\+\\+)" + }, + { + "name": "keyword.operator.arithmetic.dart", + "match": "(\\-|\\+|\\*|\\/|\\~\\/|%)" + }, + { + "name": "keyword.operator.logical.dart", + "match": "(!|&&|\\|\\|)" + }, + { + "name": "storage.modifier.dart", + "match": "(?(); private timer: NodeJS.Timer | undefined; private markdownIt: MarkdownItType.MarkdownIt | undefined; + private parse5: typeof import('parse5') | undefined; constructor() { this.disposables.push( @@ -202,8 +203,10 @@ export class ExtensionLinter { let svgStart: Diagnostic; for (const tnp of tokensAndPositions) { if (tnp.token.type === 'text' && tnp.token.content) { - const parse5 = await import('parse5'); - const parser = new parse5.SAXParser({ locationInfo: true }); + if (!this.parse5) { + this.parse5 = await import('parse5'); + } + const parser = new this.parse5.SAXParser({ locationInfo: true }); parser.on('startTag', (name, attrs, _selfClosing, location) => { if (name === 'img') { const src = attrs.find(a => a.name === 'src'); diff --git a/extensions/extension-editing/yarn.lock b/extensions/extension-editing/yarn.lock index 3275b1f2ad..01c0862412 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.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@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== "@types/node@^6.0.46": version "6.0.78" diff --git a/extensions/git/package.json b/extensions/git/package.json index 95cda8c1b7..55f83ea6a8 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -2196,6 +2196,24 @@ "highContrast": "#A7A8A9" } }, + { + "id": "gitDecoration.stageModifiedResourceForeground", + "description": "%colors.stageModified%", + "defaults": { + "light": "#895503", + "dark": "#E2C08D", + "highContrast": "#E2C08D" + } + }, + { + "id": "gitDecoration.stageDeletedResourceForeground", + "description": "%colors.stageDeleted%", + "defaults": { + "light": "#ad0707", + "dark": "#c74e39", + "highContrast": "#c74e39" + } + }, { "id": "gitDecoration.conflictingResourceForeground", "description": "%colors.conflict%", @@ -2366,11 +2384,12 @@ "devDependencies": { "@types/byline": "4.2.31", "@types/file-type": "^5.2.1", - "@types/mocha": "2.2.43", - "@types/node": "^12.12.31", - "@types/which": "^1.0.28", - "mocha": "^3.2.0", - "mocha-junit-reporter": "^1.23.3", - "mocha-multi-reporters": "^1.1.7" + "@types/mocha": "^8.2.0", + "@types/node": "14.x", + "@types/which": "^1.0.28" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" } } diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index f308aa2069..28f9ae5a43 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -197,6 +197,8 @@ "submenu.tags": "Tags", "colors.added": "Color for added resources.", "colors.modified": "Color for modified resources.", + "colors.stageModified": "Color for modified resources which have been staged.", + "colors.stageDeleted": "Color for deleted resources which have been staged.", "colors.deleted": "Color for deleted resources.", "colors.renamed": "Color for renamed or copied resources.", "colors.untracked": "Color for untracked resources.", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 5916aee083..c465cb5b65 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1947,8 +1947,7 @@ export class CommandCenter { }); const name = inputTagName.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$/g, '-'); - const message = inputMessage || name; - await repository.tag(name, message); + await repository.tag(name, inputMessage); } @command('git.deleteTag', { repository: true }) diff --git a/extensions/git/src/decorationProvider.ts b/extensions/git/src/decorationProvider.ts index 76340fe2a9..6f6788455a 100644 --- a/extensions/git/src/decorationProvider.ts +++ b/extensions/git/src/decorationProvider.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - import { window, workspace, Uri, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, ThemeColor } from 'vscode'; import * as path from 'path'; import { Repository, GitResourceGroup } from './repository'; diff --git a/extensions/git/src/decorators.ts b/extensions/git/src/decorators.ts index 62eed6d49e..5c21682c2c 100644 --- a/extensions/git/src/decorators.ts +++ b/extensions/git/src/decorators.ts @@ -98,4 +98,4 @@ export function debounce(delay: number): Function { this[timerKey] = setTimeout(() => fn.apply(this, args), delay); }; }); -} \ No newline at end of file +} diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 4a6b58cf64..ce341db291 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1969,7 +1969,7 @@ export class Repository { remote.pushUrl = url; } - // https://github.com/Microsoft/vscode/issues/45271 + // https://github.com/microsoft/vscode/issues/45271 remote.isReadOnly = remote.pushUrl === undefined || remote.pushUrl === 'no_push'; } diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index 3721bcb4d0..1421cdd5f3 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -6,8 +6,8 @@ import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); -import { ExtensionContext, workspace, window, Disposable, commands, Uri, OutputChannel } from 'vscode'; -import { findGit, Git, IGit } from './git'; +import { ExtensionContext, workspace, window, Disposable, commands, OutputChannel } from 'vscode'; +import { findGit, Git } from './git'; import { Model } from './model'; import { CommandCenter } from './commands'; import { GitFileSystemProvider } from './fileSystemProvider'; @@ -18,7 +18,7 @@ import TelemetryReporter from 'vscode-extension-telemetry'; import { GitExtension } from './api/git'; import { GitProtocolHandler } from './protocolHandler'; import { GitExtensionImpl } from './api/extension'; -// import * as path from 'path'; +// import * as path from 'path'; {{SQL CARBON EDIT}} // import * as fs from 'fs'; import * as os from 'os'; import { GitTimelineProvider } from './timelineProvider'; @@ -191,6 +191,7 @@ export async function activate(context: ExtensionContext): Promise return result; } +/* {{SQL CARBON EDIT}} async function checkGitv1(info: IGit): Promise { const config = workspace.getConfiguration('git'); const shouldIgnore = config.get('ignoreLegacyWarning') === true; @@ -246,11 +247,10 @@ async function checkGitWindows(info: IGit): Promise { } } -// @ts-expect-error async function checkGitVersion(info: IGit): Promise { await checkGitv1(info); if (process.platform === 'win32') { await checkGitWindows(info); } -} +}*/ diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index c46aff2852..c852f83f45 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -268,7 +268,7 @@ export class Model implements IRemoteSourceProviderRegistry, IPushErrorHandlerRe // This can happen whenever `path` has the wrong case sensitivity in // case insensitive file systems - // https://github.com/Microsoft/vscode/issues/33498 + // https://github.com/microsoft/vscode/issues/33498 const repositoryRoot = Uri.file(rawRoot).fsPath; if (this.getRepository(repositoryRoot)) { diff --git a/extensions/git/src/staging.ts b/extensions/git/src/staging.ts index f20243f550..4af3937255 100644 --- a/extensions/git/src/staging.ts +++ b/extensions/git/src/staging.ts @@ -18,7 +18,7 @@ export function applyLineChanges(original: TextDocument, modified: TextDocument, // if this is a deletion at the very end of the document,then we need to account // for a newline at the end of the last line which may have been deleted - // https://github.com/Microsoft/vscode/issues/59670 + // https://github.com/microsoft/vscode/issues/59670 if (isDeletion && diff.originalEndLineNumber === original.lineCount) { endLine -= 1; endCharacter = original.lineAt(endLine).range.end.character; diff --git a/extensions/git/src/test/git.test.ts b/extensions/git/src/test/git.test.ts index 842795ff7d..137be34a41 100644 --- a/extensions/git/src/test/git.test.ts +++ b/extensions/git/src/test/git.test.ts @@ -12,19 +12,19 @@ suite('git', () => { suite('GitStatusParser', () => { test('empty parser', () => { const parser = new GitStatusParser(); - assert.deepEqual(parser.status, []); + assert.deepStrictEqual(parser.status, []); }); test('empty parser 2', () => { const parser = new GitStatusParser(); parser.update(''); - assert.deepEqual(parser.status, []); + assert.deepStrictEqual(parser.status, []); }); test('simple', () => { const parser = new GitStatusParser(); parser.update('?? file.txt\0'); - assert.deepEqual(parser.status, [ + assert.deepStrictEqual(parser.status, [ { path: 'file.txt', rename: undefined, x: '?', y: '?' } ]); }); @@ -34,7 +34,7 @@ suite('git', () => { parser.update('?? file.txt\0'); parser.update('?? file2.txt\0'); parser.update('?? file3.txt\0'); - assert.deepEqual(parser.status, [ + assert.deepStrictEqual(parser.status, [ { path: 'file.txt', rename: undefined, x: '?', y: '?' }, { path: 'file2.txt', rename: undefined, x: '?', y: '?' }, { path: 'file3.txt', rename: undefined, x: '?', y: '?' } @@ -51,7 +51,7 @@ suite('git', () => { parser.update(''); parser.update('?? file3.txt\0'); parser.update(''); - assert.deepEqual(parser.status, [ + assert.deepStrictEqual(parser.status, [ { path: 'file.txt', rename: undefined, x: '?', y: '?' }, { path: 'file2.txt', rename: undefined, x: '?', y: '?' }, { path: 'file3.txt', rename: undefined, x: '?', y: '?' } @@ -61,7 +61,7 @@ suite('git', () => { test('combined', () => { const parser = new GitStatusParser(); parser.update('?? file.txt\0?? file2.txt\0?? file3.txt\0'); - assert.deepEqual(parser.status, [ + assert.deepStrictEqual(parser.status, [ { path: 'file.txt', rename: undefined, x: '?', y: '?' }, { path: 'file2.txt', rename: undefined, x: '?', y: '?' }, { path: 'file3.txt', rename: undefined, x: '?', y: '?' } @@ -72,7 +72,7 @@ suite('git', () => { const parser = new GitStatusParser(); parser.update('?? file.txt\0?? file2'); parser.update('.txt\0?? file3.txt\0'); - assert.deepEqual(parser.status, [ + assert.deepStrictEqual(parser.status, [ { path: 'file.txt', rename: undefined, x: '?', y: '?' }, { path: 'file2.txt', rename: undefined, x: '?', y: '?' }, { path: 'file3.txt', rename: undefined, x: '?', y: '?' } @@ -83,7 +83,7 @@ suite('git', () => { const parser = new GitStatusParser(); parser.update('?? file.txt'); parser.update('\0?? file2.txt\0?? file3.txt\0'); - assert.deepEqual(parser.status, [ + assert.deepStrictEqual(parser.status, [ { path: 'file.txt', rename: undefined, x: '?', y: '?' }, { path: 'file2.txt', rename: undefined, x: '?', y: '?' }, { path: 'file3.txt', rename: undefined, x: '?', y: '?' } @@ -94,7 +94,7 @@ suite('git', () => { const parser = new GitStatusParser(); parser.update('?? file.txt\0?? file2.txt\0?? file3.txt'); parser.update('\0'); - assert.deepEqual(parser.status, [ + assert.deepStrictEqual(parser.status, [ { path: 'file.txt', rename: undefined, x: '?', y: '?' }, { path: 'file2.txt', rename: undefined, x: '?', y: '?' }, { path: 'file3.txt', rename: undefined, x: '?', y: '?' } @@ -104,7 +104,7 @@ suite('git', () => { test('rename', () => { const parser = new GitStatusParser(); parser.update('R newfile.txt\0file.txt\0?? file2.txt\0?? file3.txt\0'); - assert.deepEqual(parser.status, [ + assert.deepStrictEqual(parser.status, [ { path: 'file.txt', rename: 'newfile.txt', x: 'R', y: ' ' }, { path: 'file2.txt', rename: undefined, x: '?', y: '?' }, { path: 'file3.txt', rename: undefined, x: '?', y: '?' } @@ -115,7 +115,7 @@ suite('git', () => { const parser = new GitStatusParser(); parser.update('R newfile.txt\0fil'); parser.update('e.txt\0?? file2.txt\0?? file3.txt\0'); - assert.deepEqual(parser.status, [ + assert.deepStrictEqual(parser.status, [ { path: 'file.txt', rename: 'newfile.txt', x: 'R', y: ' ' }, { path: 'file2.txt', rename: undefined, x: '?', y: '?' }, { path: 'file3.txt', rename: undefined, x: '?', y: '?' } @@ -127,7 +127,7 @@ suite('git', () => { parser.update('?? file2.txt\0R new'); parser.update('file.txt\0fil'); parser.update('e.txt\0?? file3.txt\0'); - assert.deepEqual(parser.status, [ + assert.deepStrictEqual(parser.status, [ { path: 'file2.txt', rename: undefined, x: '?', y: '?' }, { path: 'file.txt', rename: 'newfile.txt', x: 'R', y: ' ' }, { path: 'file3.txt', rename: undefined, x: '?', y: '?' } @@ -137,7 +137,7 @@ suite('git', () => { suite('parseGitmodules', () => { test('empty', () => { - assert.deepEqual(parseGitmodules(''), []); + assert.deepStrictEqual(parseGitmodules(''), []); }); test('sample', () => { @@ -146,7 +146,7 @@ suite('git', () => { url = https://github.com/gabime/spdlog.git `; - assert.deepEqual(parseGitmodules(sample), [ + assert.deepStrictEqual(parseGitmodules(sample), [ { name: 'deps/spdlog', path: 'deps/spdlog', url: 'https://github.com/gabime/spdlog.git' } ]); }); @@ -166,7 +166,7 @@ suite('git', () => { url = https://github.com/gabime/spdlog4.git `; - assert.deepEqual(parseGitmodules(sample), [ + assert.deepStrictEqual(parseGitmodules(sample), [ { name: 'deps/spdlog', path: 'deps/spdlog', url: 'https://github.com/gabime/spdlog.git' }, { name: 'deps/spdlog2', path: 'deps/spdlog2', url: 'https://github.com/gabime/spdlog.git' }, { name: 'deps/spdlog3', path: 'deps/spdlog3', url: 'https://github.com/gabime/spdlog.git' }, @@ -180,7 +180,7 @@ suite('git', () => { url = https://github.com/gabime/spdlog.git `; - assert.deepEqual(parseGitmodules(sample), [ + assert.deepStrictEqual(parseGitmodules(sample), [ { name: 'deps/spdlog', path: 'deps/spdlog', url: 'https://github.com/gabime/spdlog.git' } ]); }); @@ -191,7 +191,7 @@ suite('git', () => { url=https://github.com/gabime/spdlog.git `; - assert.deepEqual(parseGitmodules(sample), [ + assert.deepStrictEqual(parseGitmodules(sample), [ { name: 'deps/spdlog', path: 'deps/spdlog', url: 'https://github.com/gabime/spdlog.git' } ]); }); @@ -207,7 +207,7 @@ john.doe@mail.com 8e5a374372b8393906c7e380dbb09349c5385554 This is a commit message.\x00`; - assert.deepEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [{ + assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [{ hash: '52c293a05038d865604c2284aa8698bd087915a1', message: 'This is a commit message.', parents: ['8e5a374372b8393906c7e380dbb09349c5385554'], @@ -227,7 +227,7 @@ john.doe@mail.com 8e5a374372b8393906c7e380dbb09349c5385554 df27d8c75b129ab9b178b386077da2822101b217 This is a commit message.\x00`; - assert.deepEqual(parseGitCommits(GIT_OUTPUT_MULTIPLE_PARENTS), [{ + assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_MULTIPLE_PARENTS), [{ hash: '52c293a05038d865604c2284aa8698bd087915a1', message: 'This is a commit message.', parents: ['8e5a374372b8393906c7e380dbb09349c5385554', 'df27d8c75b129ab9b178b386077da2822101b217'], @@ -247,7 +247,7 @@ john.doe@mail.com This is a commit message.\x00`; - assert.deepEqual(parseGitCommits(GIT_OUTPUT_NO_PARENTS), [{ + assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_NO_PARENTS), [{ hash: '52c293a05038d865604c2284aa8698bd087915a1', message: 'This is a commit message.', parents: [], @@ -275,7 +275,7 @@ This is a commit message.\x00`; const output = parseLsTree(input); - assert.deepEqual(output, [ + assert.deepStrictEqual(output, [ { mode: '040000', type: 'tree', object: '0274a81f8ee9ca3669295dc40f510bd2021d0043', size: '-', file: '.vscode' }, { mode: '100644', type: 'blob', object: '1d487c1817262e4f20efbfa1d04c18f51b0046f6', size: '491570', file: 'Screen Shot 2018-06-01 at 14.48.05.png' }, { mode: '100644', type: 'blob', object: '686c16e4f019b734655a2576ce8b98749a9ffdb9', size: '764420', file: 'Screen Shot 2018-06-07 at 20.04.59.png' }, @@ -307,7 +307,7 @@ This is a commit message.\x00`; const output = parseLsFiles(input); - assert.deepEqual(output, [ + assert.deepStrictEqual(output, [ { mode: '100644', object: '7a73a41bfdf76d6f793007240d80983a52f15f97', stage: '0', file: '.vscode/settings.json' }, { mode: '100644', object: '1d487c1817262e4f20efbfa1d04c18f51b0046f6', stage: '0', file: 'Screen Shot 2018-06-01 at 14.48.05.png' }, { mode: '100644', object: '686c16e4f019b734655a2576ce8b98749a9ffdb9', stage: '0', file: 'Screen Shot 2018-06-07 at 20.04.59.png' }, @@ -325,72 +325,72 @@ This is a commit message.\x00`; suite('splitInChunks', () => { test('unit tests', function () { - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['hello', 'there', 'cool', 'stuff'], 6)], [['hello'], ['there'], ['cool'], ['stuff']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['hello', 'there', 'cool', 'stuff'], 10)], [['hello', 'there'], ['cool', 'stuff']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['hello', 'there', 'cool', 'stuff'], 12)], [['hello', 'there'], ['cool', 'stuff']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['hello', 'there', 'cool', 'stuff'], 14)], [['hello', 'there', 'cool'], ['stuff']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['hello', 'there', 'cool', 'stuff'], 2000)], [['hello', 'there', 'cool', 'stuff']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 1)], [['0'], ['01'], ['012'], ['0'], ['01'], ['012'], ['0'], ['01'], ['012']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 2)], [['0'], ['01'], ['012'], ['0'], ['01'], ['012'], ['0'], ['01'], ['012']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 3)], [['0', '01'], ['012'], ['0', '01'], ['012'], ['0', '01'], ['012']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 4)], [['0', '01'], ['012', '0'], ['01'], ['012', '0'], ['01'], ['012']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 5)], [['0', '01'], ['012', '0'], ['01', '012'], ['0', '01'], ['012']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 6)], [['0', '01', '012'], ['0', '01', '012'], ['0', '01', '012']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 7)], [['0', '01', '012', '0'], ['01', '012', '0'], ['01', '012']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 8)], [['0', '01', '012', '0'], ['01', '012', '0', '01'], ['012']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 9)], [['0', '01', '012', '0', '01'], ['012', '0', '01', '012']] ); diff --git a/extensions/git/src/test/smoke.test.ts b/extensions/git/src/test/smoke.test.ts index f8caf097c3..8b53053c96 100644 --- a/extensions/git/src/test/smoke.test.ts +++ b/extensions/git/src/test/smoke.test.ts @@ -59,8 +59,8 @@ suite('git smoke test', function () { await eventToPromise(git.onDidOpenRepository); } - assert.equal(git.repositories.length, 1); - assert.equal(fs.realpathSync(git.repositories[0].rootUri.fsPath), cwd); + assert.strictEqual(git.repositories.length, 1); + assert.strictEqual(fs.realpathSync(git.repositories[0].rootUri.fsPath), cwd); repository = git.repositories[0]; }); @@ -72,7 +72,7 @@ suite('git smoke test', function () { await type(appjs, ' world'); await appjs.save(); await repository.status(); - assert.equal(repository.state.workingTreeChanges.length, 1); + assert.strictEqual(repository.state.workingTreeChanges.length, 1); repository.state.workingTreeChanges.some(r => r.uri.path === appjs.uri.path && r.status === Status.MODIFIED); fs.writeFileSync(file('newfile.txt'), ''); @@ -80,7 +80,7 @@ suite('git smoke test', function () { await type(newfile, 'hey there'); await newfile.save(); await repository.status(); - assert.equal(repository.state.workingTreeChanges.length, 2); + assert.strictEqual(repository.state.workingTreeChanges.length, 2); repository.state.workingTreeChanges.some(r => r.uri.path === appjs.uri.path && r.status === Status.MODIFIED); repository.state.workingTreeChanges.some(r => r.uri.path === newfile.uri.path && r.status === Status.UNTRACKED); }); @@ -90,7 +90,7 @@ suite('git smoke test', function () { await commands.executeCommand('git.openChange', appjs); assert(window.activeTextEditor); - assert.equal(window.activeTextEditor!.document.uri.path, appjs.path); + assert.strictEqual(window.activeTextEditor!.document.uri.path, appjs.path); // TODO: how do we really know this is a diff editor? }); @@ -100,13 +100,13 @@ suite('git smoke test', function () { const newfile = uri('newfile.txt'); await commands.executeCommand('git.stage', appjs); - assert.equal(repository.state.workingTreeChanges.length, 1); + assert.strictEqual(repository.state.workingTreeChanges.length, 1); repository.state.workingTreeChanges.some(r => r.uri.path === newfile.path && r.status === Status.UNTRACKED); - assert.equal(repository.state.indexChanges.length, 1); + assert.strictEqual(repository.state.indexChanges.length, 1); repository.state.indexChanges.some(r => r.uri.path === appjs.path && r.status === Status.INDEX_MODIFIED); await commands.executeCommand('git.unstage', appjs); - assert.equal(repository.state.workingTreeChanges.length, 2); + assert.strictEqual(repository.state.workingTreeChanges.length, 2); repository.state.workingTreeChanges.some(r => r.uri.path === appjs.path && r.status === Status.MODIFIED); repository.state.workingTreeChanges.some(r => r.uri.path === newfile.path && r.status === Status.UNTRACKED); }); @@ -117,14 +117,14 @@ suite('git smoke test', function () { await commands.executeCommand('git.stage', appjs); await repository.commit('second commit'); - assert.equal(repository.state.workingTreeChanges.length, 1); + assert.strictEqual(repository.state.workingTreeChanges.length, 1); repository.state.workingTreeChanges.some(r => r.uri.path === newfile.path && r.status === Status.UNTRACKED); - assert.equal(repository.state.indexChanges.length, 0); + assert.strictEqual(repository.state.indexChanges.length, 0); await commands.executeCommand('git.stageAll', appjs); await repository.commit('third commit'); - assert.equal(repository.state.workingTreeChanges.length, 0); - assert.equal(repository.state.indexChanges.length, 0); + assert.strictEqual(repository.state.workingTreeChanges.length, 0); + assert.strictEqual(repository.state.indexChanges.length, 0); }); test('rename/delete conflict', async function () { diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index 2250130ae8..f4aae746f5 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -353,7 +353,7 @@ export class Limiter { } queue(factory: () => Promise): Promise { - return new Promise((c, e) => { + return new Promise((c, e) => { this.outstandingPromises.push({ factory, c, e }); this.consume(); }); diff --git a/extensions/git/yarn.lock b/extensions/git/yarn.lock index b2708ace3a..38fccc8542 100644 --- a/extensions/git/yarn.lock +++ b/extensions/git/yarn.lock @@ -16,31 +16,26 @@ dependencies: "@types/node" "*" -"@types/mocha@2.2.43": - version "2.2.43" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.43.tgz#03c54589c43ad048cbcbfd63999b55d0424eec27" - integrity sha512-xNlAmH+lRJdUMXClMTI9Y0pRqIojdxfm7DHsIxoB2iTzu3fnPmSMEN8SsSx0cdwV36d02PWCWaDUoZPDSln+xw== +"@types/mocha@^8.2.0": + version "8.2.3" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.3.tgz#bbeb55fbc73f28ea6de601fbfa4613f58d785323" + integrity sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw== "@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.12.31": - version "12.12.31" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.31.tgz#d6b4f9645fee17f11319b508fb1001797425da51" - integrity sha512-T+wnJno8uh27G9c+1T+a1/WYCHzLeDqtsGJkoEdSp2X8RTh3oOCZQcUnjAx90CS8cmmADX51O0FI/tu9s0yssg== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== "@types/which@^1.0.28": version "1.0.28" resolved "https://registry.yarnpkg.com/@types/which/-/which-1.0.28.tgz#016e387629b8817bed653fe32eab5d11279c8df6" integrity sha1-AW44dim4gXvtZT/jLqtdESecjfY= -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.7.4: version "1.7.4" resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.7.4.tgz#e7d96435594d893b00cf49f70a5927105dbb8749" @@ -66,34 +61,11 @@ async-listener@^0.6.0: semver "^5.3.0" shimmer "^1.1.0" -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.8" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" - integrity sha1-wHshHHyVLsH479Uad+8NHTmQopI= - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -browser-stdout@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" - integrity sha1-81HTKWnTL6XXpVZxVCY9korjvR8= - byline@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= -charenc@~0.0.1: - version "0.0.2" - resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" - integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= - cls-hooked@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/cls-hooked/-/cls-hooked-4.2.2.tgz#ad2e9a4092680cdaffeb2d3551da0e225eae1908" @@ -103,18 +75,6 @@ cls-hooked@^4.2.2: emitter-listener "^1.0.1" semver "^5.4.1" -commander@2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" - integrity sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q= - dependencies: - graceful-readlink ">= 1.0.0" - -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= - continuation-local-storage@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz#11f613f74e914fe9b34c92ad2d28fe6ae1db7ffb" @@ -123,32 +83,6 @@ continuation-local-storage@^3.2.1: async-listener "^0.6.0" emitter-listener "^1.1.1" -crypt@~0.0.1: - version "0.0.2" - resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" - integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= - -debug@2.6.8: - version "2.6.8" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" - integrity sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw= - dependencies: - ms "2.0.0" - -debug@^2.2.0: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^3.1.0: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - diagnostic-channel-publishers@^0.3.3: version "0.3.5" resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.3.5.tgz#a84a05fd6cc1d7619fdd17791c17e540119a7536" @@ -161,11 +95,6 @@ diagnostic-channel@0.2.0: dependencies: semver "^5.3.0" -diff@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" - integrity sha1-yc45Okt8vQsFinJck98pkCeGj/k= - emitter-listener@^1.0.1, emitter-listener@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8" @@ -173,76 +102,16 @@ emitter-listener@^1.0.1, emitter-listener@^1.1.1: dependencies: shimmer "^1.2.0" -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= - file-type@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/file-type/-/file-type-7.2.0.tgz#113cfed52e1d6959ab80248906e2f25a8cdccb74" integrity sha1-ETz+1S4daVmrgCSJBuLyWozcy3Q= -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -glob@7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" - integrity sha1-gFIR3wT6rxxjo2ADBs31reULLsg= - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.2" - once "^1.3.0" - path-is-absolute "^1.0.0" - -"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= - -growl@1.9.2: - version "1.9.2" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" - integrity sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8= - -has-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" - integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo= - -he@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" - integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= - 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== -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -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== - isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -253,159 +122,6 @@ jschardet@2.3.0: resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-2.3.0.tgz#06e2636e16c8ada36feebbdc08aa34e6a9b3ff75" integrity sha512-6I6xT7XN/7sBB7q8ObzKbmv5vN+blzLcboDE1BNEsEfmRXJValMxO6OIRT69ylPBRemS3rw6US+CMCar0OBc9g== -json3@3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" - integrity sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE= - -lodash._baseassign@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" - integrity sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4= - dependencies: - lodash._basecopy "^3.0.0" - lodash.keys "^3.0.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._basecreate@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821" - integrity sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE= - -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.create@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7" - integrity sha1-1/KEnw29p+BGgruM1yqwIkYd6+c= - dependencies: - lodash._baseassign "^3.0.0" - lodash._basecreate "^3.0.0" - lodash._isiterateecall "^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@^4.16.4: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== - -md5@^2.1.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" - integrity sha1-U6s41f48iJG6RlMp6iP6wFQBJvk= - dependencies: - charenc "~0.0.1" - crypt "~0.0.1" - is-buffer "~1.1.1" - -minimatch@^3.0.2: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= - -mkdirp@0.5.1, 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" - -mocha-junit-reporter@^1.23.3: - version "1.23.3" - resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-1.23.3.tgz#941e219dd759ed732f8641e165918aa8b167c981" - integrity sha512-ed8LqbRj1RxZfjt/oC9t12sfrWsjZ3gNnbhV1nuj9R/Jb5/P3Xb4duv2eCfCDMYH+fEu0mqca7m4wsiVjsxsvA== - dependencies: - debug "^2.2.0" - md5 "^2.1.0" - mkdirp "~0.5.1" - strip-ansi "^4.0.0" - xml "^1.0.0" - -mocha-multi-reporters@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/mocha-multi-reporters/-/mocha-multi-reporters-1.1.7.tgz#cc7f3f4d32f478520941d852abb64d9988587d82" - integrity sha1-zH8/TTL0eFIJQdhSq7ZNmYhYfYI= - dependencies: - debug "^3.1.0" - lodash "^4.16.4" - -mocha@^3.2.0: - version "3.5.3" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.5.3.tgz#1e0480fe36d2da5858d1eb6acc38418b26eaa20d" - integrity sha512-/6na001MJWEtYxHOV1WLfsmR4YIynkUEhBwzsb+fk2qmQ3iqsi258l/Q2MWHJMImAcNpZ8DEdYAK72NHoIQ9Eg== - dependencies: - browser-stdout "1.3.0" - commander "2.9.0" - debug "2.6.8" - diff "3.2.0" - escape-string-regexp "1.0.5" - glob "7.1.1" - growl "1.9.2" - he "1.1.1" - json3 "3.3.2" - lodash.create "3.1.1" - mkdirp "0.5.1" - supports-color "3.1.2" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - semver@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" @@ -426,20 +142,6 @@ stack-chain@^1.3.7: resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-1.3.7.tgz#d192c9ff4ea6a22c94c4dd459171e3f00cea1285" integrity sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU= -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -supports-color@3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" - integrity sha1-cqJiiU2dQIuVbKBf83su2KbiotU= - dependencies: - has-flag "^1.0.0" - vscode-extension-telemetry@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.7.tgz#18389bc24127c89dade29cd2b71ba69a6ee6ad26" @@ -463,13 +165,3 @@ which@^1.3.0: integrity sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg== dependencies: 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= - -xml@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" - integrity sha1-eLpyAgApxbyHuKgaPPzXS0ovweU= diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index 4a18a5bd0a..2064e28ded 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -4,7 +4,7 @@ "description": "%description%", "publisher": "vscode", "license": "MIT", - "version": "0.0.1", + "version": "0.0.2", "engines": { "vscode": "^1.41.0" }, @@ -19,7 +19,8 @@ "web" ], "activationEvents": [ - "onAuthenticationRequest:github" + "onAuthenticationRequest:github", + "onAuthenticationRequest:github-enterprise" ], "capabilities": { "virtualWorkspaces": true, @@ -31,7 +32,14 @@ "commands": [ { "command": "github.provide-token", - "title": "Manually Provide Token" + "title": "Manually Provide Token", + "category": "GitHub" + }, + { + "command": "github-enterprise.provide-token", + "title": "Manually Provide Token", + "category": "GitHub Enterprise" + } ], "menus": { @@ -39,6 +47,10 @@ { "command": "github.provide-token", "when": "false" + }, + { + "command": "github-enterprise.provide-token", + "when": "false" } ] }, @@ -46,8 +58,21 @@ { "label": "GitHub", "id": "github" + }, + { + "label": "GitHub Enterprise", + "id": "github-enterprise" } - ] + ], + "configuration": { + "title": "GitHub Enterprise Authentication Provider", + "properties": { + "github-enterprise.uri" : { + "type": "string", + "description": "URI of your GitHub Enterprise Instanace" + } + } + } }, "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", "main": "./out/extension.js", @@ -67,7 +92,7 @@ "vscode-tas-client": "^0.1.22" }, "devDependencies": { - "@types/node": "^12.19.9", + "@types/node": "14.x", "@types/node-fetch": "^2.5.7", "@types/uuid": "8.0.0" }, diff --git a/extensions/github-authentication/src/common/keychain.ts b/extensions/github-authentication/src/common/keychain.ts index 959e340140..591920b24d 100644 --- a/extensions/github-authentication/src/common/keychain.ts +++ b/extensions/github-authentication/src/common/keychain.ts @@ -28,13 +28,11 @@ export type Keytar = { deletePassword: typeof keytarType['deletePassword']; }; -const SERVICE_ID = `github.auth`; - export class Keychain { - constructor(private context: vscode.ExtensionContext) { } + constructor(private context: vscode.ExtensionContext, private serviceId: string) { } async setToken(token: string): Promise { try { - return await this.context.secrets.store(SERVICE_ID, token); + return await this.context.secrets.store(this.serviceId, token); } catch (e) { // Ignore Logger.error(`Setting token failed: ${e}`); @@ -48,7 +46,7 @@ export class Keychain { async getToken(): Promise { try { - return await this.context.secrets.get(SERVICE_ID); + return await this.context.secrets.get(this.serviceId); } catch (e) { // Ignore Logger.error(`Getting token failed: ${e}`); @@ -58,7 +56,7 @@ export class Keychain { async deleteToken(): Promise { try { - return await this.context.secrets.delete(SERVICE_ID); + return await this.context.secrets.delete(this.serviceId); } catch (e) { // Ignore Logger.error(`Deleting token failed: ${e}`); diff --git a/extensions/github-authentication/src/experimentationService.ts b/extensions/github-authentication/src/experimentationService.ts index 8d66edbdda..fd82f647a8 100644 --- a/extensions/github-authentication/src/experimentationService.ts +++ b/extensions/github-authentication/src/experimentationService.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; diff --git a/extensions/github-authentication/src/extension.ts b/extensions/github-authentication/src/extension.ts index f2de676d9a..8e2d340bfb 100644 --- a/extensions/github-authentication/src/extension.ts +++ b/extensions/github-authentication/src/extension.ts @@ -4,9 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { GitHubAuthenticationProvider, onDidChangeSessions } from './github'; -import { uriHandler } from './githubServer'; -import Logger from './common/logger'; +import { GitHubAuthenticationProvider, AuthProviderType } from './github'; import TelemetryReporter from 'vscode-extension-telemetry'; import { createExperimentationService, ExperimentationTelemetry } from './experimentationService'; @@ -17,74 +15,13 @@ export async function activate(context: vscode.ExtensionContext) { const experimentationService = await createExperimentationService(context, telemetryReporter); await experimentationService.initialFetch; - context.subscriptions.push(vscode.window.registerUriHandler(uriHandler)); - const loginService = new GitHubAuthenticationProvider(context, telemetryReporter); - - await loginService.initialize(context); - - context.subscriptions.push(vscode.commands.registerCommand('github.provide-token', () => { - return loginService.manuallyProvideToken(); - })); - - context.subscriptions.push(vscode.authentication.registerAuthenticationProvider('github', 'GitHub', { - onDidChangeSessions: onDidChangeSessions.event, - getSessions: (scopes?: string[]) => loginService.getSessions(scopes), - createSession: async (scopeList: string[]) => { - try { - /* __GDPR__ - "login" : { } - */ - telemetryReporter.sendTelemetryEvent('login'); - - const session = await loginService.createSession(scopeList.sort().join(' ')); - Logger.info('Login success!'); - onDidChangeSessions.fire({ added: [session], removed: [], changed: [] }); - return session; - } catch (e) { - // If login was cancelled, do not notify user. - if (e.message === 'Cancelled') { - /* __GDPR__ - "loginCancelled" : { } - */ - telemetryReporter.sendTelemetryEvent('loginCancelled'); - throw e; - } - - /* __GDPR__ - "loginFailed" : { } - */ - telemetryReporter.sendTelemetryEvent('loginFailed'); - - vscode.window.showErrorMessage(`Sign in failed: ${e}`); - Logger.error(e); - throw e; - } - }, - removeSession: async (id: string) => { - try { - /* __GDPR__ - "logout" : { } - */ - telemetryReporter.sendTelemetryEvent('logout'); - - const session = await loginService.removeSession(id); - if (session) { - onDidChangeSessions.fire({ added: [], removed: [session], changed: [] }); - } - } catch (e) { - /* __GDPR__ - "logoutFailed" : { } - */ - telemetryReporter.sendTelemetryEvent('logoutFailed'); - - vscode.window.showErrorMessage(`Sign out failed: ${e}`); - Logger.error(e); - throw e; - } - } - }, { supportsMultipleAccounts: false })); - - return; + [ + AuthProviderType.github, + AuthProviderType['github-enterprise'] + ].forEach(async type => { + const loginService = new GitHubAuthenticationProvider(context, type, telemetryReporter); + await loginService.initialize(); + }); } // this method is called when your extension is deactivated diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index c20e063a0a..3831a263b2 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -6,13 +6,11 @@ import * as vscode from 'vscode'; import { v4 as uuid } from 'uuid'; import { Keychain } from './common/keychain'; -import { GitHubServer, NETWORK_ERROR } from './githubServer'; +import { GitHubServer, uriHandler, NETWORK_ERROR } from './githubServer'; import Logger from './common/logger'; import { arrayEquals } from './common/utils'; import { ExperimentationTelemetry } from './experimentationService'; -export const onDidChangeSessions = new vscode.EventEmitter(); - interface SessionData { id: string; account?: { @@ -24,18 +22,29 @@ interface SessionData { accessToken: string; } -export class GitHubAuthenticationProvider { +export enum AuthProviderType { + github = 'github', + 'github-enterprise' = 'github-enterprise' +} + + +export class GitHubAuthenticationProvider implements vscode.AuthenticationProvider { private _sessions: vscode.AuthenticationSession[] = []; + private _sessionChangeEmitter = new vscode.EventEmitter(); private _githubServer: GitHubServer; private _keychain: Keychain; - constructor(context: vscode.ExtensionContext, telemetryReporter: ExperimentationTelemetry) { - this._keychain = new Keychain(context); - this._githubServer = new GitHubServer(telemetryReporter); + constructor(private context: vscode.ExtensionContext, private type: AuthProviderType, private telemetryReporter: ExperimentationTelemetry) { + this._keychain = new Keychain(context, `${type}.auth`); + this._githubServer = new GitHubServer(type, telemetryReporter); } - public async initialize(context: vscode.ExtensionContext): Promise { + get onDidChangeSessions() { + return this._sessionChangeEmitter.event; + } + + public async initialize(): Promise { try { this._sessions = await this.readSessions(); await this.verifySessions(); @@ -43,7 +52,17 @@ export class GitHubAuthenticationProvider { // Ignore, network request failed } - context.subscriptions.push(context.secrets.onDidChange(() => this.checkForUpdates())); + let friendlyName = 'GitHub'; + if (this.type === AuthProviderType.github) { + this.context.subscriptions.push(vscode.window.registerUriHandler(uriHandler)); + } + if (this.type === AuthProviderType['github-enterprise']) { + friendlyName = 'GitHub Enterprise'; + } + + this.context.subscriptions.push(vscode.commands.registerCommand(`${this.type}.provide-token`, () => this.manuallyProvideToken())); + this.context.subscriptions.push(vscode.authentication.registerAuthenticationProvider(this.type, friendlyName, this, { supportsMultipleAccounts: false })); + this.context.subscriptions.push(this.context.secrets.onDidChange(() => this.checkForUpdates())); } async getSessions(scopes?: string[]): Promise { @@ -52,12 +71,21 @@ export class GitHubAuthenticationProvider { : this._sessions; } + private async afterTokenLoad(token: string): Promise { + if (this.type === AuthProviderType.github) { + this._githubServer.checkIsEdu(token); + } + if (this.type === AuthProviderType['github-enterprise']) { + this._githubServer.checkEnterpriseVersion(token); + } + } + private async verifySessions(): Promise { const verifiedSessions: vscode.AuthenticationSession[] = []; const verificationPromises = this._sessions.map(async session => { try { await this._githubServer.getUserInfo(session.accessToken); - this._githubServer.checkIsEdu(session.accessToken); + this.afterTokenLoad(session.accessToken); verifiedSessions.push(session); } catch (e) { // Remove sessions that return unauthorized response @@ -97,7 +125,7 @@ export class GitHubAuthenticationProvider { } }); - this._sessions.map(session => { + this._sessions.forEach(session => { const matchesExisting = storedSessions.some(s => s.id === session.id); // Another window has logged out, remove from our state if (!matchesExisting) { @@ -112,7 +140,7 @@ export class GitHubAuthenticationProvider { }); if (added.length || removed.length) { - onDidChangeSessions.fire({ added, removed, changed: [] }); + this._sessionChangeEmitter.fire({ added, removed, changed: [] }); } } @@ -163,12 +191,41 @@ export class GitHubAuthenticationProvider { return this._sessions; } - public async createSession(scopes: string): Promise { - const token = await this._githubServer.login(scopes); - const session = await this.tokenToSession(token, scopes.split(' ')); - this._githubServer.checkIsEdu(token); - await this.setToken(session); - return session; + public async createSession(scopes: string[]): Promise { + try { + /* __GDPR__ + "login" : { } + */ + this.telemetryReporter?.sendTelemetryEvent('login'); + + const token = await this._githubServer.login(scopes.join(' ')); + this.afterTokenLoad(token); + const session = await this.tokenToSession(token, scopes); + await this.setToken(session); + this._sessionChangeEmitter.fire({ added: [session], removed: [], changed: [] }); + + Logger.info('Login success!'); + + return session; + } catch (e) { + // If login was cancelled, do not notify user. + if (e.message === 'Cancelled') { + /* __GDPR__ + "loginCancelled" : { } + */ + this.telemetryReporter?.sendTelemetryEvent('loginCancelled'); + throw e; + } + + /* __GDPR__ + "loginFailed" : { } + */ + this.telemetryReporter?.sendTelemetryEvent('loginFailed'); + + vscode.window.showErrorMessage(`Sign in failed: ${e}`); + Logger.error(e); + throw e; + } } public async manuallyProvideToken(): Promise { @@ -196,18 +253,33 @@ export class GitHubAuthenticationProvider { await this.storeSessions(); } - public async removeSession(id: string): Promise { - Logger.info(`Logging out of ${id}`); - const sessionIndex = this._sessions.findIndex(session => session.id === id); - let session: vscode.AuthenticationSession | undefined; - if (sessionIndex > -1) { - session = this._sessions[sessionIndex]; - this._sessions.splice(sessionIndex, 1); - } else { - Logger.error('Session not found'); - } + public async removeSession(id: string) { + try { + /* __GDPR__ + "logout" : { } + */ + this.telemetryReporter?.sendTelemetryEvent('logout'); - await this.storeSessions(); - return session; + Logger.info(`Logging out of ${id}`); + const sessionIndex = this._sessions.findIndex(session => session.id === id); + if (sessionIndex > -1) { + const session = this._sessions[sessionIndex]; + this._sessions.splice(sessionIndex, 1); + this._sessionChangeEmitter.fire({ added: [], removed: [session], changed: [] }); + } else { + Logger.error('Session not found'); + } + + await this.storeSessions(); + } catch (e) { + /* __GDPR__ + "logoutFailed" : { } + */ + this.telemetryReporter?.sendTelemetryEvent('logoutFailed'); + + vscode.window.showErrorMessage(`Sign out failed: ${e}`); + Logger.error(e); + throw e; + } } } diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index 1bd09ea4ad..bd224c1a0c 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -10,6 +10,7 @@ import { v4 as uuid } from 'uuid'; import { PromiseAdapter, promiseFromEvent } from './common/utils'; import Logger from './common/logger'; import { ExperimentationTelemetry } from './experimentationService'; +import { AuthProviderType } from './github'; const localize = nls.loadMessageBundle(); @@ -25,10 +26,6 @@ class UriEventHandler extends vscode.EventEmitter implements vscode. export const uriHandler = new UriEventHandler; -const onDidManuallyProvideToken = new vscode.EventEmitter(); - - - function parseQuery(uri: vscode.Uri) { return uri.query.split('&').reduce((prev: any, current) => { const queryString = current.split('='); @@ -39,20 +36,21 @@ function parseQuery(uri: vscode.Uri) { export class GitHubServer { private _statusBarItem: vscode.StatusBarItem | undefined; + private _onDidManuallyProvideToken = new vscode.EventEmitter(); private _pendingStates = new Map(); private _codeExchangePromises = new Map, cancel: vscode.EventEmitter }>(); - constructor(private readonly telemetryReporter: ExperimentationTelemetry) { } + constructor(private type: AuthProviderType, private readonly telemetryReporter: ExperimentationTelemetry) { } private isTestEnvironment(url: vscode.Uri): boolean { - return /\.azurewebsites\.net$/.test(url.authority) || url.authority.startsWith('localhost:'); + return this.type === AuthProviderType['github-enterprise'] || /\.azurewebsites\.net$/.test(url.authority) || url.authority.startsWith('localhost:'); } // TODO@joaomoreno TODO@RMacfarlane private async isNoCorsEnvironment(): Promise { - const uri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate`)); - return uri.scheme === 'https' && /^vscode\./.test(uri.authority); + const uri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/dummy`)); + return (uri.scheme === 'https' && /^vscode\./.test(uri.authority)) || (uri.scheme === 'http' && /^localhost/.test(uri.authority)); } public async login(scopes: string): Promise { @@ -104,7 +102,7 @@ export class GitHubServer { return Promise.race([ codeExchangePromise.promise, - promiseFromEvent(onDidManuallyProvideToken.event, (token: string | undefined, resolve, reject): void => { + promiseFromEvent(this._onDidManuallyProvideToken.event, (token: string | undefined, resolve, reject): void => { if (!token) { reject('Cancelled'); } else { @@ -164,11 +162,31 @@ export class GitHubServer { } }; + private getServerUri(path?: string) { + const apiUri = this.type === AuthProviderType['github-enterprise'] + ? vscode.Uri.parse(vscode.workspace.getConfiguration('github-enterprise').get('uri') || '', true) + : vscode.Uri.parse('https://api.github.com'); + + if (!path) { + path = ''; + } + if (this.type === AuthProviderType['github-enterprise']) { + path = '/api/v3' + path; + } + + return vscode.Uri.parse(`${apiUri.scheme}://${apiUri.authority}${path}`); + } + private updateStatusBarItem(isStart?: boolean) { if (isStart && !this._statusBarItem) { - this._statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); - this._statusBarItem.text = localize('signingIn', "$(mark-github) Signing in to github.com..."); - this._statusBarItem.command = 'github.provide-token'; + this._statusBarItem = vscode.window.createStatusBarItem('status.git.signIn', vscode.StatusBarAlignment.Left); + this._statusBarItem.name = localize('status.git.signIn.name', "GitHub Sign-in"); + this._statusBarItem.text = this.type === AuthProviderType.github + ? localize('signingIn', "$(mark-github) Signing in to github.com...") + : localize('signingInEnterprise', "$(mark-github) Signing in to {0}...", this.getServerUri().authority); + this._statusBarItem.command = this.type === AuthProviderType.github + ? 'github.provide-token' + : 'github-enterprise.provide-token'; this._statusBarItem.show(); } @@ -181,7 +199,7 @@ export class GitHubServer { public async manuallyProvideToken() { const uriOrToken = await vscode.window.showInputBox({ prompt: 'Token', ignoreFocusOut: true }); if (!uriOrToken) { - onDidManuallyProvideToken.fire(undefined); + this._onDidManuallyProvideToken.fire(undefined); return; } @@ -192,14 +210,14 @@ export class GitHubServer { } catch (e) { // If it doesn't look like a URI, treat it as a token. Logger.info('Treating input as token'); - onDidManuallyProvideToken.fire(uriOrToken); + this._onDidManuallyProvideToken.fire(uriOrToken); } } private async getScopes(token: string): Promise { try { Logger.info('Getting token scopes...'); - const result = await fetch('https://api.github.com', { + const result = await fetch(this.getServerUri('/').toString(), { headers: { Authorization: `token ${token}`, 'User-Agent': 'Visual-Studio-Code' @@ -223,7 +241,7 @@ export class GitHubServer { let result: Response; try { Logger.info('Getting user info...'); - result = await fetch('https://api.github.com/user', { + result = await fetch(this.getServerUri('/user').toString(), { headers: { Authorization: `token ${token}`, 'User-Agent': 'Visual-Studio-Code' @@ -279,6 +297,34 @@ export class GitHubServer { } catch (e) { // No-op } + } + public async checkEnterpriseVersion(token: string): Promise { + try { + + const result = await fetch(this.getServerUri('/meta').toString(), { + headers: { + Authorization: `token ${token}`, + 'User-Agent': 'Visual-Studio-Code' + } + }); + + if (!result.ok) { + return; + } + + const json: { verifiable_password_authentication: boolean, installed_version: string } = await result.json(); + + /* __GDPR__ + "ghe-session" : { + "version": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryReporter.sendTelemetryEvent('ghe-session', { + version: json.installed_version + }); + } catch { + // No-op + } } } diff --git a/extensions/github-authentication/yarn.lock b/extensions/github-authentication/yarn.lock index 8095e57452..090dc94dcb 100644 --- a/extensions/github-authentication/yarn.lock +++ b/extensions/github-authentication/yarn.lock @@ -15,10 +15,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.5.tgz#3d03acd3b3414cf67faf999aed11682ed121f22b" integrity sha512-90hiq6/VqtQgX8Sp0EzeIsv3r+ellbGj4URKj5j30tLlZvRUpnAe9YbYnjl3pJM93GyXU0tghHhvXHq+5rnCKA== -"@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@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== "@types/uuid@8.0.0": version "8.0.0" diff --git a/extensions/github/package.json b/extensions/github/package.json index 8f67314b45..f21080ab6f 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -70,7 +70,7 @@ "vscode-nls": "^4.1.2" }, "devDependencies": { - "@types/node": "^12.19.9" + "@types/node": "14.x" }, "repository": { "type": "git", diff --git a/extensions/github/yarn.lock b/extensions/github/yarn.lock index 05a0b4cf6f..94f7a26da9 100644 --- a/extensions/github/yarn.lock +++ b/extensions/github/yarn.lock @@ -99,16 +99,16 @@ dependencies: "@types/node" ">= 8" +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== + "@types/node@>= 8": version "14.0.23" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.23.tgz#676fa0883450ed9da0bb24156213636290892806" integrity sha512-Z4U8yDAl5TFkmYsZdFPdjeMa57NOvnaf1tljHzhouaPEp7LCj2JKkejpI1ODviIAQuW4CcQmxkQ77rnLsOOoKw== -"@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" resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" diff --git a/extensions/image-preview/src/binarySizeStatusBarEntry.ts b/extensions/image-preview/src/binarySizeStatusBarEntry.ts index c339a84de5..0e23ca16f8 100644 --- a/extensions/image-preview/src/binarySizeStatusBarEntry.ts +++ b/extensions/image-preview/src/binarySizeStatusBarEntry.ts @@ -39,12 +39,7 @@ class BinarySize { export class BinarySizeStatusBarEntry extends PreviewStatusBarEntry { constructor() { - super({ - id: 'imagePreview.binarySize', - name: localize('sizeStatusBar.name', "Image Binary Size"), - alignment: vscode.StatusBarAlignment.Right, - priority: 100, - }); + super('status.imagePreview.binarySize', localize('sizeStatusBar.name', "Image Binary Size"), vscode.StatusBarAlignment.Right, 100); } public show(owner: string, size: number | undefined) { diff --git a/extensions/image-preview/src/ownedStatusBarEntry.ts b/extensions/image-preview/src/ownedStatusBarEntry.ts index 6de4eec020..bd394ec3d1 100644 --- a/extensions/image-preview/src/ownedStatusBarEntry.ts +++ b/extensions/image-preview/src/ownedStatusBarEntry.ts @@ -11,9 +11,10 @@ export abstract class PreviewStatusBarEntry extends Disposable { protected readonly entry: vscode.StatusBarItem; - constructor(options: vscode.StatusBarItemOptions) { + constructor(id: string, name: string, alignment: vscode.StatusBarAlignment, priority: number) { super(); - this.entry = this._register(vscode.window.createStatusBarItem(options)); + this.entry = this._register(vscode.window.createStatusBarItem(id, alignment, priority)); + this.entry.name = name; } protected showItem(owner: string, text: string) { diff --git a/extensions/image-preview/src/sizeStatusBarEntry.ts b/extensions/image-preview/src/sizeStatusBarEntry.ts index a2e7e50a89..73ce2eecf7 100644 --- a/extensions/image-preview/src/sizeStatusBarEntry.ts +++ b/extensions/image-preview/src/sizeStatusBarEntry.ts @@ -12,12 +12,7 @@ const localize = nls.loadMessageBundle(); export class SizeStatusBarEntry extends PreviewStatusBarEntry { constructor() { - super({ - id: 'imagePreview.size', - name: localize('sizeStatusBar.name', "Image Size"), - alignment: vscode.StatusBarAlignment.Right, - priority: 101 /* to the left of editor status (100) */, - }); + super('status.imagePreview.size', localize('sizeStatusBar.name', "Image Size"), vscode.StatusBarAlignment.Right, 101 /* to the left of editor status (100) */); } public show(owner: string, text: string) { diff --git a/extensions/image-preview/src/zoomStatusBarEntry.ts b/extensions/image-preview/src/zoomStatusBarEntry.ts index dfe166ae3b..2563755070 100644 --- a/extensions/image-preview/src/zoomStatusBarEntry.ts +++ b/extensions/image-preview/src/zoomStatusBarEntry.ts @@ -19,12 +19,7 @@ export class ZoomStatusBarEntry extends OwnedStatusBarEntry { public readonly onDidChangeScale = this._onDidChangeScale.event; constructor() { - super({ - id: 'imagePreview.zoom', - name: localize('zoomStatusBar.name', "Image Zoom"), - alignment: vscode.StatusBarAlignment.Right, - priority: 102 /* to the left of editor size entry (101) */, - }); + super('status.imagePreview.zoom', localize('zoomStatusBar.name', "Image Zoom"), vscode.StatusBarAlignment.Right, 102 /* to the left of editor size entry (101) */); this._register(vscode.commands.registerCommand(selectZoomLevelCommandId, async () => { type MyPickItem = vscode.QuickPickItem & { scale: Scale }; diff --git a/extensions/json-language-features/CONTRIBUTING.md b/extensions/json-language-features/CONTRIBUTING.md index 90367dec71..7203d02e6f 100644 --- a/extensions/json-language-features/CONTRIBUTING.md +++ b/extensions/json-language-features/CONTRIBUTING.md @@ -1,12 +1,12 @@ ## Setup -- Clone [Microsoft/vscode](https://github.com/microsoft/vscode) +- Clone [microsoft/vscode](https://github.com/microsoft/vscode) - Run `yarn` at `/`, this will install - Dependencies for `/extension/json-language-features/` - Dependencies for `/extension/json-language-features/server/` - devDependencies such as `gulp` - Open `/extensions/json-language-features/` as the workspace in VS Code -- Run the [`Launch Extension`](https://github.com/Microsoft/vscode/blob/master/extensions/json-language-features/.vscode/launch.json) debug target in the Debug View. This will: +- Run the [`Launch Extension`](https://github.com/microsoft/vscode/blob/master/extensions/json-language-features/.vscode/launch.json) debug target in the Debug View. This will: - Launch the `preLaunchTask` task to compile the extension - Launch a new VS Code instance with the `json-language-features` extension loaded - You should see a notification saying the development version of `json-language-features` overwrites the bundled version of `json-language-features` @@ -18,15 +18,15 @@ ### Contribute to vscode-json-languageservice -[Microsoft/vscode-json-languageservice](https://github.com/Microsoft/vscode-json-languageservice) is the library that implements the language smarts for JSON. +[microsoft/vscode-json-languageservice](https://github.com/microsoft/vscode-json-languageservice) is the library that implements the language smarts for JSON. The JSON language server forwards most the of requests to the service library. -If you want to fix JSON issues or make improvements, you should make changes at [Microsoft/vscode-json-languageservice](https://github.com/Microsoft/vscode-json-languageservice). +If you want to fix JSON issues or make improvements, you should make changes at [microsoft/vscode-json-languageservice](https://github.com/microsoft/vscode-json-languageservice). However, within this extension, you can run a development version of `vscode-json-languageservice` to debug code or test language features interactively: #### Linking `vscode-json-languageservice` in `json-language-features/server/` -- Clone [Microsoft/vscode-json-languageservice](https://github.com/Microsoft/vscode-json-languageservice) +- Clone [microsoft/vscode-json-languageservice](https://github.com/microsoft/vscode-json-languageservice) - Run `npm install` in `vscode-json-languageservice` - Run `npm link` in `vscode-json-languageservice`. This will compile and link `vscode-json-languageservice` - In `json-language-features/server/`, run `yarn link vscode-json-languageservice` @@ -36,4 +36,4 @@ However, within this extension, you can run a development version of `vscode-jso - Open both `vscode-json-languageservice` and this extension in a single workspace with [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) feature - Run `yarn watch` at `json-languagefeatures/server/` to recompile this extension with the linked version of `vscode-json-languageservice` - Make some changes in `vscode-json-languageservice` -- Now when you run `Launch Extension` debug target, the launched instance will use your development version of `vscode-json-languageservice`. You can interactively test the language features. \ No newline at end of file +- Now when you run `Launch Extension` debug target, the launched instance will use your development version of `vscode-json-languageservice`. You can interactively test the language features. diff --git a/extensions/json-language-features/client/src/jsonClient.ts b/extensions/json-language-features/client/src/jsonClient.ts index 5ecfe4b760..f4e0fc1e31 100644 --- a/extensions/json-language-features/client/src/jsonClient.ts +++ b/extensions/json-language-features/client/src/jsonClient.ts @@ -7,7 +7,7 @@ import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); import { - workspace, window, languages, commands, ExtensionContext, extensions, Uri, LanguageConfiguration, + workspace, window, languages, commands, ExtensionContext, extensions, Uri, Diagnostic, StatusBarAlignment, TextEditor, TextDocument, FormattingOptions, CancellationToken, ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString, } from 'vscode'; @@ -101,12 +101,8 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua const documentSelector = ['json', 'jsonc']; - const schemaResolutionErrorStatusBarItem = window.createStatusBarItem({ - id: 'status.json.resolveError', - name: localize('json.resolveError', "JSON: Schema Resolution Error"), - alignment: StatusBarAlignment.Right, - priority: 0, - }); + const schemaResolutionErrorStatusBarItem = window.createStatusBarItem('status.json.resolveError', StatusBarAlignment.Right, 0); + schemaResolutionErrorStatusBarItem.name = localize('json.resolveError', "JSON: Schema Resolution Error"); schemaResolutionErrorStatusBarItem.text = '$(alert)'; toDispose.push(schemaResolutionErrorStatusBarItem); @@ -362,17 +358,6 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua } }); - - const languageConfiguration: LanguageConfiguration = { - wordPattern: /("(?:[^\\\"]*(?:\\.)?)*"?)|[^\s{}\[\],:]+/, - indentationRules: { - increaseIndentPattern: /({+(?=([^"]*"[^"]*")*[^"}]*$))|(\[+(?=([^"]*"[^"]*")*[^"\]]*$))/, - decreaseIndentPattern: /^\s*[}\]],?\s*$/ - } - }; - languages.setLanguageConfiguration('json', languageConfiguration); - languages.setLanguageConfiguration('jsonc', languageConfiguration); - } function getSchemaAssociations(_context: ExtensionContext): ISchemaAssociation[] { diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index b9814d8988..4b832e4ebb 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -26,7 +26,6 @@ "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": [ @@ -141,7 +140,7 @@ "vscode-nls": "^5.0.0" }, "devDependencies": { - "@types/node": "^12.19.9" + "@types/node": "14.x" }, "repository": { "type": "git", diff --git a/extensions/json-language-features/server/README.md b/extensions/json-language-features/server/README.md index 4530898488..328a523f5a 100644 --- a/extensions/json-language-features/server/README.md +++ b/extensions/json-language-features/server/README.md @@ -218,14 +218,14 @@ To connect to the server from NodeJS, see Remy Suen's great write-up on [how to ## Participate -The source code of the JSON language server can be found in the [VSCode repository](https://github.com/Microsoft/vscode) at [extensions/json-language-features/server](https://github.com/Microsoft/vscode/tree/master/extensions/json-language-features/server). +The source code of the JSON language server can be found in the [VSCode repository](https://github.com/microsoft/vscode) at [extensions/json-language-features/server](https://github.com/microsoft/vscode/tree/master/extensions/json-language-features/server). -File issues and pull requests in the [VSCode GitHub Issues](https://github.com/Microsoft/vscode/issues). See the document [How to Contribute](https://github.com/Microsoft/vscode/wiki/How-to-Contribute) on how to build and run from source. +File issues and pull requests in the [VSCode GitHub Issues](https://github.com/microsoft/vscode/issues). See the document [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) on how to build and run from source. Most of the functionality of the server is located in libraries: -- [jsonc-parser](https://github.com/Microsoft/node-jsonc-parser) contains the JSON parser and scanner. -- [vscode-json-languageservice](https://github.com/Microsoft/vscode-json-languageservice) contains the implementation of all features as a re-usable library. -- [vscode-languageserver-node](https://github.com/Microsoft/vscode-languageserver-node) contains the implementation of language server for NodeJS. +- [jsonc-parser](https://github.com/microsoft/node-jsonc-parser) contains the JSON parser and scanner. +- [vscode-json-languageservice](https://github.com/microsoft/vscode-json-languageservice) contains the implementation of all features as a re-usable library. +- [vscode-languageserver-node](https://github.com/microsoft/vscode-languageserver-node) contains the implementation of language server for NodeJS. Help on any of these projects is very welcome. diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index e1051c4ee8..49926f97fa 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": "^4.1.2", + "vscode-json-languageservice": "^4.1.4", "vscode-languageserver": "^7.0.0", "vscode-uri": "^3.0.2" }, "devDependencies": { "@types/mocha": "^8.2.0", - "@types/node": "^12.19.9" + "@types/node": "14.x" }, "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 e7a5e11e7f..93a1290eca 100644 --- a/extensions/json-language-features/server/src/jsonServer.ts +++ b/extensions/json-language-features/server/src/jsonServer.ts @@ -6,7 +6,7 @@ import { Connection, TextDocuments, InitializeParams, InitializeResult, NotificationType, RequestType, - DocumentRangeFormattingRequest, Disposable, ServerCapabilities, TextDocumentSyncKind, TextEdit + DocumentRangeFormattingRequest, Disposable, ServerCapabilities, TextDocumentSyncKind, TextEdit, DocumentFormattingRequest, TextDocumentIdentifier, FormattingOptions } from 'vscode-languageserver'; import { formatError, runSafe, runSafeAsync } from './utils/runner'; @@ -138,6 +138,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) hoverProvider: true, documentSymbolProvider: true, documentRangeFormattingProvider: params.initializationOptions?.provideFormatter === true, + documentFormattingProvider: params.initializationOptions?.provideFormatter === true, colorProvider: {}, foldingRangeProvider: true, selectionRangeProvider: true, @@ -206,7 +207,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) let jsonConfigurationSettings: JSONSchemaSettings[] | undefined = undefined; let schemaAssociations: ISchemaAssociations | SchemaConfiguration[] | undefined = undefined; - let formatterRegistration: Thenable | null = null; + let formatterRegistrations: Thenable[] | null = null; // The settings have changed. Is send on server activation as well. connection.onDidChangeConfiguration((change) => { @@ -224,12 +225,16 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) if (dynamicFormatterRegistration) { const enableFormatter = settings && settings.json && settings.json.format && settings.json.format.enable; if (enableFormatter) { - if (!formatterRegistration) { - formatterRegistration = connection.client.register(DocumentRangeFormattingRequest.type, { documentSelector: [{ language: 'json' }, { language: 'jsonc' }] }); + if (!formatterRegistrations) { + const documentSelector = [{ language: 'json' }, { language: 'jsonc' }]; + formatterRegistrations = [ + connection.client.register(DocumentRangeFormattingRequest.type, { documentSelector }), + connection.client.register(DocumentFormattingRequest.type, { documentSelector }) + ]; } - } else if (formatterRegistration) { - formatterRegistration.then(r => r.dispose()); - formatterRegistration = null; + } else if (formatterRegistrations) { + formatterRegistrations.forEach(p => p.then(r => r.dispose())); + formatterRegistrations = null; } } }); @@ -420,19 +425,25 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) }, [], `Error while computing document symbols for ${documentSymbolParams.textDocument.uri}`, token); }); - connection.onDocumentRangeFormatting((formatParams, token) => { - return runSafe(() => { - const document = documents.get(formatParams.textDocument.uri); - if (document) { - const edits = languageService.format(document, formatParams.range, formatParams.options); - if (edits.length > formatterMaxNumberOfEdits) { - const newText = TextDocument.applyEdits(document, edits); - return [TextEdit.replace(Range.create(Position.create(0, 0), document.positionAt(document.getText().length)), newText)]; - } - return edits; + function onFormat(textDocument: TextDocumentIdentifier, range: Range | undefined, options: FormattingOptions): TextEdit[] { + const document = documents.get(textDocument.uri); + if (document) { + const edits = languageService.format(document, range ?? getFullRange(document), options); + if (edits.length > formatterMaxNumberOfEdits) { + const newText = TextDocument.applyEdits(document, edits); + return [TextEdit.replace(getFullRange(document), newText)]; } - return []; - }, [], `Error while formatting range for ${formatParams.textDocument.uri}`, token); + return edits; + } + return []; + } + + connection.onDocumentRangeFormatting((formatParams, token) => { + return runSafe(() => onFormat(formatParams.textDocument, formatParams.range, formatParams.options), [], `Error while formatting range for ${formatParams.textDocument.uri}`, token); + }); + + connection.onDocumentFormatting((formatParams, token) => { + return runSafe(() => onFormat(formatParams.textDocument, undefined, formatParams.options), [], `Error while formatting ${formatParams.textDocument.uri}`, token); }); connection.onDocumentColor((params, token) => { @@ -495,3 +506,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) // Listen on the connection connection.listen(); } + +function getFullRange(document: TextDocument): Range { + return Range.create(Position.create(0, 0), document.positionAt(document.getText().length)); +} diff --git a/extensions/json-language-features/server/src/languageModelCache.ts b/extensions/json-language-features/server/src/languageModelCache.ts index 81bf9dfb42..a30dbfb924 100644 --- a/extensions/json-language-features/server/src/languageModelCache.ts +++ b/extensions/json-language-features/server/src/languageModelCache.ts @@ -79,4 +79,4 @@ export function getLanguageModelCache(maxEntries: number, cleanupIntervalTime } } }; -} \ No newline at end of file +} diff --git a/extensions/json-language-features/server/yarn.lock b/extensions/json-language-features/server/yarn.lock index 5c6a160f8c..abe012c86a 100644 --- a/extensions/json-language-features/server/yarn.lock +++ b/extensions/json-language-features/server/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.0.tgz#3eb56d13a1de1d347ecb1957c6860c911704bc44" integrity sha512-/Sge3BymXo4lKc31C8OINJgXLaw+7vL1/L1pGiBNpGrBiT8FQiaFpSYV0uhTaG4y78vcMBTMFsWaHDvuD+xGzQ== -"@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@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== agent-base@4: version "4.1.2" @@ -105,10 +105,10 @@ request-light@^0.4.0: https-proxy-agent "^2.2.4" vscode-nls "^4.1.2" -vscode-json-languageservice@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-4.1.2.tgz#c3873f791e23488a8b373e02c85c232bd625e27a" - integrity sha512-atAz6m4UZCslB7yk03Qb3/MKn1E5l07063syAEUfKRcVKTVN3t1+/KDlGu1nVCRUFAgz5+18gKidQvO4PVPLdg== +vscode-json-languageservice@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-4.1.4.tgz#c83d3d812f8f17ab525724c611d8ff5e8834fc84" + integrity sha512-/UqaE58BVFdePM9l971L6xPRLlCLNk01aovf1Pp9hB/8pytmd2s9ZNEnS1JqYyQEJ1k5/fEBsWOdhQlNo4H7VA== dependencies: jsonc-parser "^3.0.0" minimatch "^3.0.4" diff --git a/extensions/json-language-features/yarn.lock b/extensions/json-language-features/yarn.lock index a4533a6e53..4ba9b7b630 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.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@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== agent-base@4: version "4.2.1" diff --git a/extensions/json/build/update-grammars.js b/extensions/json/build/update-grammars.js index 37a8fe5810..18a2916325 100644 --- a/extensions/json/build/update-grammars.js +++ b/extensions/json/build/update-grammars.js @@ -31,7 +31,7 @@ function adaptJSON(grammar, replacementScope) { } } -var tsGrammarRepo = 'Microsoft/vscode-JSON.tmLanguage'; +var tsGrammarRepo = 'microsoft/vscode-JSON.tmLanguage'; updateGrammar.update(tsGrammarRepo, 'JSON.tmLanguage', './syntaxes/JSON.tmLanguage.json'); updateGrammar.update(tsGrammarRepo, 'JSON.tmLanguage', './syntaxes/JSONC.tmLanguage.json', grammar => adaptJSON(grammar, '.json.comments')); diff --git a/extensions/json/cgmanifest.json b/extensions/json/cgmanifest.json index fabb7a93ab..1af8426e53 100644 --- a/extensions/json/cgmanifest.json +++ b/extensions/json/cgmanifest.json @@ -4,8 +4,8 @@ "component": { "type": "git", "git": { - "name": "Microsoft/vscode-JSON.tmLanguage", - "repositoryUrl": "https://github.com/Microsoft/vscode-JSON.tmLanguage", + "name": "microsoft/vscode-JSON.tmLanguage", + "repositoryUrl": "https://github.com/microsoft/vscode-JSON.tmLanguage", "commitHash": "9bd83f1c252b375e957203f21793316203f61f70" } }, diff --git a/extensions/json/language-configuration.json b/extensions/json/language-configuration.json index 7faa70cef7..8f270cd2b2 100644 --- a/extensions/json/language-configuration.json +++ b/extensions/json/language-configuration.json @@ -14,5 +14,10 @@ { "open": "'", "close": "'", "notIn": ["string"] }, { "open": "\"", "close": "\"", "notIn": ["string", "comment"] }, { "open": "`", "close": "`", "notIn": ["string", "comment"] } - ] + ], + "wordPattern": "(\"(?:[^\\\\\\\"]*(?:\\\\.)?)*\"?)|[^\\s{}\\[\\],:]+", + "indentationRules": { + "increaseIndentPattern": "({+(?=([^\"]*\"[^\"]*\")*[^\"}]*$))|(\\[+(?=([^\"]*\"[^\"]*\")*[^\"\\]]*$))", + "decreaseIndentPattern": "^\\s*[}\\]],?\\s*$" + } } diff --git a/extensions/json/package.json b/extensions/json/package.json index a4ccc12cf2..8f197c3118 100644 --- a/extensions/json/package.json +++ b/extensions/json/package.json @@ -29,7 +29,8 @@ ".ts.map", ".har", ".jslintrc", - ".jsonld" + ".jsonld", + ".ipynb" ], "filenames": [ "composer.lock", @@ -57,6 +58,8 @@ ".babelrc" ], "filenames": [ + "babel.config.json", + ".babelrc.json", ".ember-cli" ], "configuration": "./language-configuration.json" diff --git a/extensions/json/syntaxes/JSON.tmLanguage.json b/extensions/json/syntaxes/JSON.tmLanguage.json index 910045be39..b53febdc8a 100644 --- a/extensions/json/syntaxes/JSON.tmLanguage.json +++ b/extensions/json/syntaxes/JSON.tmLanguage.json @@ -1,10 +1,10 @@ { "information_for_contributors": [ - "This file has been converted from https://github.com/Microsoft/vscode-JSON.tmLanguage/blob/master/JSON.tmLanguage", + "This file has been converted from https://github.com/microsoft/vscode-JSON.tmLanguage/blob/master/JSON.tmLanguage", "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-JSON.tmLanguage/commit/9bd83f1c252b375e957203f21793316203f61f70", + "version": "https://github.com/microsoft/vscode-JSON.tmLanguage/commit/9bd83f1c252b375e957203f21793316203f61f70", "name": "JSON (Javascript Next)", "scopeName": "source.json", "patterns": [ diff --git a/extensions/json/syntaxes/JSONC.tmLanguage.json b/extensions/json/syntaxes/JSONC.tmLanguage.json index 50028ef0f3..31828ba65b 100644 --- a/extensions/json/syntaxes/JSONC.tmLanguage.json +++ b/extensions/json/syntaxes/JSONC.tmLanguage.json @@ -1,10 +1,10 @@ { "information_for_contributors": [ - "This file has been converted from https://github.com/Microsoft/vscode-JSON.tmLanguage/blob/master/JSON.tmLanguage", + "This file has been converted from https://github.com/microsoft/vscode-JSON.tmLanguage/blob/master/JSON.tmLanguage", "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-JSON.tmLanguage/commit/9bd83f1c252b375e957203f21793316203f61f70", + "version": "https://github.com/microsoft/vscode-JSON.tmLanguage/commit/9bd83f1c252b375e957203f21793316203f61f70", "name": "JSON with comments", "scopeName": "source.json.comments", "patterns": [ diff --git a/extensions/julia/cgmanifest.json b/extensions/julia/cgmanifest.json index 3b2f492cc6..e2f4268ed2 100644 --- a/extensions/julia/cgmanifest.json +++ b/extensions/julia/cgmanifest.json @@ -4,7 +4,7 @@ "component": { "type": "git", "git": { - "name": " JuliaEditorSupport/atom-language-julia", + "name": "JuliaEditorSupport/atom-language-julia", "repositoryUrl": "https://github.com/JuliaEditorSupport/atom-language-julia", "commitHash": "008e02c5ec9440fa9f0ea8a891712c7238f24706" } @@ -14,4 +14,4 @@ } ], "version": 1 -} \ No newline at end of file +} diff --git a/extensions/markdown-basics/cgmanifest.json b/extensions/markdown-basics/cgmanifest.json index 92288d403b..f3f0717c5a 100644 --- a/extensions/markdown-basics/cgmanifest.json +++ b/extensions/markdown-basics/cgmanifest.json @@ -33,7 +33,7 @@ "git": { "name": "microsoft/vscode-markdown-tm-grammar", "repositoryUrl": "https://github.com/microsoft/vscode-markdown-tm-grammar", - "commitHash": "7019b191c3ee38b6c345f3a2a843f223eb92ca1e" + "commitHash": "a612b96d62aa1ce305c4a55dc9d577316fab39da" } }, "license": "MIT", diff --git a/extensions/markdown-basics/language-configuration.json b/extensions/markdown-basics/language-configuration.json index 7dade386d0..d5d64a80ae 100644 --- a/extensions/markdown-basics/language-configuration.json +++ b/extensions/markdown-basics/language-configuration.json @@ -49,5 +49,6 @@ "start": "^\\s*", "end": "^\\s*" } - } + }, + "wordPattern": { "pattern": "(\\p{Alphabetic}|\\p{Number}|\\p{Nonspacing_Mark})(((\\p{Alphabetic}|\\p{Number}|\\p{Nonspacing_Mark})|[_])?(\\p{Alphabetic}|\\p{Number}|\\p{Nonspacing_Mark}))*", "flags": "ug" }, } diff --git a/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json b/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json index a61af0d0c0..aaa4c774b4 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/7019b191c3ee38b6c345f3a2a843f223eb92ca1e", + "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/a612b96d62aa1ce305c4a55dc9d577316fab39da", "name": "Markdown", "scopeName": "text.html.markdown", "patterns": [ @@ -63,7 +63,7 @@ "while": "(^|\\G)\\s*(>) ?" }, "fenced_code_block_css": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(css|css.erb)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(css|css.erb)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -96,7 +96,7 @@ ] }, "fenced_code_block_basic": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(html|htm|shtml|xhtml|inc|tmpl|tpl)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(html|htm|shtml|xhtml|inc|tmpl|tpl)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -129,7 +129,7 @@ ] }, "fenced_code_block_ini": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(ini|conf)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(ini|conf)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -162,7 +162,7 @@ ] }, "fenced_code_block_java": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(java|bsh)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(java|bsh)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -195,7 +195,7 @@ ] }, "fenced_code_block_lua": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(lua)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(lua)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -228,7 +228,7 @@ ] }, "fenced_code_block_makefile": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(Makefile|makefile|GNUmakefile|OCamlMakefile)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(Makefile|makefile|GNUmakefile|OCamlMakefile)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -261,7 +261,7 @@ ] }, "fenced_code_block_perl": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(perl|pl|pm|pod|t|PL|psgi|vcl)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(perl|pl|pm|pod|t|PL|psgi|vcl)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -294,7 +294,7 @@ ] }, "fenced_code_block_r": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(R|r|s|S|Rprofile|\\{\\.r.+?\\})((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(R|r|s|S|Rprofile|\\{\\.r.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -327,7 +327,7 @@ ] }, "fenced_code_block_ruby": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(ruby|rb|rbx|rjs|Rakefile|rake|cgi|fcgi|gemspec|irbrc|Capfile|ru|prawn|Cheffile|Gemfile|Guardfile|Hobofile|Vagrantfile|Appraisals|Rantfile|Berksfile|Berksfile.lock|Thorfile|Puppetfile)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(ruby|rb|rbx|rjs|Rakefile|rake|cgi|fcgi|gemspec|irbrc|Capfile|ru|prawn|Cheffile|Gemfile|Guardfile|Hobofile|Vagrantfile|Appraisals|Rantfile|Berksfile|Berksfile.lock|Thorfile|Puppetfile)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -360,7 +360,7 @@ ] }, "fenced_code_block_php": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(php|php3|php4|php5|phpt|phtml|aw|ctp)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(php|php3|php4|php5|phpt|phtml|aw|ctp)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -396,7 +396,7 @@ ] }, "fenced_code_block_sql": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(sql|ddl|dml)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(sql|ddl|dml)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -429,7 +429,7 @@ ] }, "fenced_code_block_vs_net": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(vb)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(vb)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -462,7 +462,7 @@ ] }, "fenced_code_block_xml": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(xml|xsd|tld|jsp|pt|cpt|dtml|rss|opml)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(xml|xsd|tld|jsp|pt|cpt|dtml|rss|opml)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -495,7 +495,7 @@ ] }, "fenced_code_block_xsl": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(xsl|xslt)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(xsl|xslt)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -528,7 +528,7 @@ ] }, "fenced_code_block_yaml": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(yaml|yml)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(yaml|yml)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -561,7 +561,7 @@ ] }, "fenced_code_block_dosbatch": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(bat|batch)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(bat|batch)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -594,7 +594,7 @@ ] }, "fenced_code_block_clojure": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(clj|cljs|clojure)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(clj|cljs|clojure)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -627,7 +627,7 @@ ] }, "fenced_code_block_coffee": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(coffee|Cakefile|coffee.erb)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(coffee|Cakefile|coffee.erb)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -660,7 +660,7 @@ ] }, "fenced_code_block_c": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(c|h)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(c|h)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -693,7 +693,7 @@ ] }, "fenced_code_block_cpp": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(cpp|c\\+\\+|cxx)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(cpp|c\\+\\+|cxx)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -726,7 +726,7 @@ ] }, "fenced_code_block_diff": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(patch|diff|rej)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(patch|diff|rej)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -759,7 +759,7 @@ ] }, "fenced_code_block_dockerfile": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(dockerfile|Dockerfile)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(dockerfile|Dockerfile)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -792,7 +792,7 @@ ] }, "fenced_code_block_git_commit": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(COMMIT_EDITMSG|MERGE_MSG)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(COMMIT_EDITMSG|MERGE_MSG)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -825,7 +825,7 @@ ] }, "fenced_code_block_git_rebase": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(git-rebase-todo)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(git-rebase-todo)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -858,7 +858,7 @@ ] }, "fenced_code_block_go": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(go|golang)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(go|golang)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -891,7 +891,7 @@ ] }, "fenced_code_block_groovy": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(groovy|gvy)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(groovy|gvy)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -924,7 +924,7 @@ ] }, "fenced_code_block_pug": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(jade|pug)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(jade|pug)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -957,7 +957,7 @@ ] }, "fenced_code_block_js": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(js|jsx|javascript|es6|mjs|cjs|\\{\\.js.+?\\})((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(js|jsx|javascript|es6|mjs|cjs|\\{\\.js.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -990,7 +990,7 @@ ] }, "fenced_code_block_js_regexp": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(regexp)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(regexp)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1023,7 +1023,7 @@ ] }, "fenced_code_block_json": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(json|json5|sublime-settings|sublime-menu|sublime-keymap|sublime-mousemap|sublime-theme|sublime-build|sublime-project|sublime-completions)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(json|json5|sublime-settings|sublime-menu|sublime-keymap|sublime-mousemap|sublime-theme|sublime-build|sublime-project|sublime-completions)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1056,7 +1056,7 @@ ] }, "fenced_code_block_jsonc": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(jsonc)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(jsonc)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1089,7 +1089,7 @@ ] }, "fenced_code_block_less": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(less)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(less)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1122,7 +1122,7 @@ ] }, "fenced_code_block_objc": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(objectivec|objective-c|mm|objc|obj-c|m|h)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(objectivec|objective-c|mm|objc|obj-c|m|h)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1155,7 +1155,7 @@ ] }, "fenced_code_block_swift": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(swift)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(swift)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1188,7 +1188,7 @@ ] }, "fenced_code_block_scss": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(scss)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(scss)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1221,7 +1221,7 @@ ] }, "fenced_code_block_perl6": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(perl6|p6|pl6|pm6|nqp)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(perl6|p6|pl6|pm6|nqp)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1254,7 +1254,7 @@ ] }, "fenced_code_block_powershell": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(powershell|ps1|psm1|psd1)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(powershell|ps1|psm1|psd1)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1287,7 +1287,7 @@ ] }, "fenced_code_block_python": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(python|py|py3|rpy|pyw|cpy|SConstruct|Sconstruct|sconstruct|SConscript|gyp|gypi|\\{\\.python.+?\\})((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(python|py|py3|rpy|pyw|cpy|SConstruct|Sconstruct|sconstruct|SConscript|gyp|gypi|\\{\\.python.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1320,7 +1320,7 @@ ] }, "fenced_code_block_regexp_python": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(re)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(re)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1353,7 +1353,7 @@ ] }, "fenced_code_block_rust": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(rust|rs|\\{\\.rust.+?\\})((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(rust|rs|\\{\\.rust.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1386,7 +1386,7 @@ ] }, "fenced_code_block_scala": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(scala|sbt)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(scala|sbt)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1419,7 +1419,7 @@ ] }, "fenced_code_block_shell": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(shell|sh|bash|zsh|bashrc|bash_profile|bash_login|profile|bash_logout|.textmate_init|\\{\\.bash.+?\\})((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(shell|sh|bash|zsh|bashrc|bash_profile|bash_login|profile|bash_logout|.textmate_init|\\{\\.bash.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1452,7 +1452,7 @@ ] }, "fenced_code_block_ts": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(typescript|ts)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(typescript|ts)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1485,7 +1485,7 @@ ] }, "fenced_code_block_tsx": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(tsx)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(tsx)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1518,7 +1518,7 @@ ] }, "fenced_code_block_csharp": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(cs|csharp|c#)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(cs|csharp|c#)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1551,7 +1551,7 @@ ] }, "fenced_code_block_fsharp": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(fs|fsharp|f#)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(fs|fsharp|f#)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1584,7 +1584,7 @@ ] }, "fenced_code_block_dart": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(dart)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(dart)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1617,7 +1617,7 @@ ] }, "fenced_code_block_handlebars": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(handlebars|hbs)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(handlebars|hbs)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1650,7 +1650,7 @@ ] }, "fenced_code_block_markdown": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(markdown|md)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(markdown|md)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1683,7 +1683,7 @@ ] }, "fenced_code_block_log": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(log)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(log)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1716,7 +1716,7 @@ ] }, "fenced_code_block_erlang": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(erlang)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(erlang)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1749,7 +1749,7 @@ ] }, "fenced_code_block_elixir": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(elixir)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(elixir)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1975,7 +1975,15 @@ "name": "punctuation.definition.heading.markdown" }, "2": { - "name": "entity.name.section.markdown" + "name": "entity.name.section.markdown", + "patterns": [ + { + "include": "#inline" + }, + { + "include": "text.html.derivative" + } + ] }, "3": { "name": "punctuation.definition.heading.markdown" @@ -1990,7 +1998,15 @@ "name": "punctuation.definition.heading.markdown" }, "2": { - "name": "entity.name.section.markdown" + "name": "entity.name.section.markdown", + "patterns": [ + { + "include": "#inline" + }, + { + "include": "text.html.derivative" + } + ] }, "3": { "name": "punctuation.definition.heading.markdown" @@ -2005,7 +2021,15 @@ "name": "punctuation.definition.heading.markdown" }, "2": { - "name": "entity.name.section.markdown" + "name": "entity.name.section.markdown", + "patterns": [ + { + "include": "#inline" + }, + { + "include": "text.html.derivative" + } + ] }, "3": { "name": "punctuation.definition.heading.markdown" @@ -2020,7 +2044,15 @@ "name": "punctuation.definition.heading.markdown" }, "2": { - "name": "entity.name.section.markdown" + "name": "entity.name.section.markdown", + "patterns": [ + { + "include": "#inline" + }, + { + "include": "text.html.derivative" + } + ] }, "3": { "name": "punctuation.definition.heading.markdown" @@ -2035,7 +2067,15 @@ "name": "punctuation.definition.heading.markdown" }, "2": { - "name": "entity.name.section.markdown" + "name": "entity.name.section.markdown", + "patterns": [ + { + "include": "#inline" + }, + { + "include": "text.html.derivative" + } + ] }, "3": { "name": "punctuation.definition.heading.markdown" @@ -2050,7 +2090,15 @@ "name": "punctuation.definition.heading.markdown" }, "2": { - "name": "entity.name.section.markdown" + "name": "entity.name.section.markdown", + "patterns": [ + { + "include": "#inline" + }, + { + "include": "text.html.derivative" + } + ] }, "3": { "name": "punctuation.definition.heading.markdown" diff --git a/extensions/markdown-language-features/esbuild.js b/extensions/markdown-language-features/esbuild.js index 202e467704..6b8afbed45 100644 --- a/extensions/markdown-language-features/esbuild.js +++ b/extensions/markdown-language-features/esbuild.js @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ const path = require('path'); const esbuild = require('esbuild'); diff --git a/extensions/markdown-language-features/media/index.js b/extensions/markdown-language-features/media/index.js deleted file mode 100644 index 6dad4cbb5f..0000000000 --- a/extensions/markdown-language-features/media/index.js +++ /dev/null @@ -1 +0,0 @@ -!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=(0,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(!(0,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=(0,a.getSettings)(),d=acquireVsCodeApi(),m=d.getState(),g={..."object"==typeof m?m:{},...(0,a.getData)("data-state")};d.setState(g);const p=(0,r.createPosterForVsCode)(d);function h(t){const n=document.getElementsByTagName("img");if(n.length>0){const o=Array.from(n,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()},(0,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=(0,s.getLineElementForFragment)(f.fragment);e&&(u+=1,(0,s.scrollToRevealSourceLine)(e.line))}else isNaN(f.line)||(u+=1,(0,s.scrollToRevealSourceLine)(f.line))}):h(()=>{u+=1,window.scrollTo(0,e*document.body.clientHeight)})});const v=(()=>{const e=c(e=>{u+=1,h(()=>(0,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=(0,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=(0,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:(0,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/markdown.css b/extensions/markdown-language-features/media/markdown.css index 042302a7b9..4200d7193a 100644 --- a/extensions/markdown-language-features/media/markdown.css +++ b/extensions/markdown-language-features/media/markdown.css @@ -106,6 +106,13 @@ body.showEditorSelection li.code-line:hover:before { border-left: none; } +ul ul, +ul ol, +ol ul, +ol ol { + margin-bottom: 0; +} + img { max-width: 100%; max-height: 100%; @@ -152,6 +159,7 @@ h1 { table { border-collapse: collapse; + margin-bottom: 0.7em; } th { diff --git a/extensions/markdown-language-features/media/pre.js b/extensions/markdown-language-features/media/pre.js deleted file mode 100644 index 365deaa62d..0000000000 --- a/extensions/markdown-language-features/media/pre.js +++ /dev/null @@ -1 +0,0 @@ -!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=(0,s.getStrings)(),t=(0,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/notebook/index.ts b/extensions/markdown-language-features/notebook/index.ts index 1d7c652334..be5715b093 100644 --- a/extensions/markdown-language-features/notebook/index.ts +++ b/extensions/markdown-language-features/notebook/index.ts @@ -1,39 +1,177 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ const MarkdownIt = require('markdown-it'); -export async function activate(ctx: { - dependencies: ReadonlyArray<{ entrypoint: string }> -}) { +export function activate() { let markdownIt = new MarkdownIt({ html: true }); - // Should we load the deps before this point? - // Also could we await inside `renderMarkup`? - await Promise.all(ctx.dependencies.map(async (dep) => { - try { - const api = await import(dep.entrypoint); - if (api?.extendMarkdownIt) { - markdownIt = api.extendMarkdownIt(markdownIt); - } - } catch (e) { - console.error('Could not load markdown entryPoint', e); + const style = document.createElement('style'); + style.classList.add('markdown-style'); + style.textContent = ` + .emptyMarkdownCell::before { + content: "${document.documentElement.style.getPropertyValue('--notebook-cell-markup-empty-content')}"; + font-style: italic; + opacity: 0.6; } - })); + + img { + max-width: 100%; + max-height: 100%; + } + + a { + text-decoration: none; + } + + a:hover { + text-decoration: underline; + } + + a:focus, + input:focus, + select:focus, + textarea:focus { + outline: 1px solid -webkit-focus-ring-color; + outline-offset: -1px; + } + + hr { + border: 0; + height: 2px; + border-bottom: 2px solid; + } + + h1 { + font-size: 26px; + line-height: 31px; + margin: 0; + margin-bottom: 13px; + } + + h2 { + font-size: 19px; + margin: 0; + margin-bottom: 10px; + } + + h1, + h2, + h3 { + font-weight: normal; + } + + div { + width: 100%; + } + + /* Adjust margin of first item in markdown cell */ + *:first-child { + margin-top: 0px; + } + + /* h1 tags don't need top margin */ + h1:first-child { + margin-top: 0; + } + + /* Removes bottom margin when only one item exists in markdown cell */ + *:only-child, + *:last-child { + margin-bottom: 0; + padding-bottom: 0; + } + + /* makes all markdown cells consistent */ + div { + min-height: var(--notebook-markdown-min-height); + } + + table { + border-collapse: collapse; + border-spacing: 0; + } + + table th, + table td { + border: 1px solid; + } + + table > thead > tr > th { + text-align: left; + border-bottom: 1px solid; + } + + table > thead > tr > th, + table > thead > tr > td, + table > tbody > tr > th, + table > tbody > tr > td { + padding: 5px 10px; + } + + table > tbody > tr + tr > td { + border-top: 1px solid; + } + + blockquote { + margin: 0 7px 0 5px; + padding: 0 16px 0 10px; + border-left-width: 5px; + border-left-style: solid; + } + + code, + .code { + font-size: 1em; + line-height: 1.357em; + } + + .code { + white-space: pre-wrap; + } + `; + document.head.append(style); return { - renderMarkup: (context: { element: HTMLElement, content: string }) => { - const rendered = markdownIt.render(context.content); - context.element.innerHTML = rendered; + renderOutputItem: (outputInfo: { text(): string }, element: HTMLElement) => { + let previewNode: HTMLElement; + if (!element.shadowRoot) { + const previewRoot = element.attachShadow({ mode: 'open' }); - // Insert styles into markdown preview shadow dom so that they are applied - for (const markdownStyleNode of document.getElementsByClassName('markdown-style')) { - context.element.appendChild(markdownStyleNode.cloneNode(true)); + // Insert styles into markdown preview shadow dom so that they are applied. + // First add default webview style + const defaultStyles = document.getElementById('_defaultStyles') as HTMLStyleElement; + previewRoot.appendChild(defaultStyles.cloneNode(true)); + + // And then contributed styles + for (const markdownStyleNode of document.getElementsByClassName('markdown-style')) { + previewRoot.appendChild(markdownStyleNode.cloneNode(true)); + } + + previewNode = document.createElement('div'); + previewNode.id = 'preview'; + previewRoot.appendChild(previewNode); + } else { + previewNode = element.shadowRoot.getElementById('preview')!; } + + const text = outputInfo.text(); + if (text.trim().length === 0) { + previewNode.innerText = ''; + previewNode.classList.add('emptyMarkdownCell'); + } else { + previewNode.classList.remove('emptyMarkdownCell'); + + const rendered = markdownIt.render(text); + previewNode.innerHTML = rendered; + } + }, + extendMarkdownIt: (f: (md: typeof markdownIt) => void) => { + f(markdownIt); } }; } diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 19d1354c20..61d2a84808 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -26,6 +26,7 @@ "onCommand:markdown.showSource", "onCommand:markdown.showPreviewSecuritySelector", "onCommand:markdown.api.render", + "onCommand:markdown.api.reloadPlugins", "onWebviewPanel:markdown.preview", "onCustomEditor:vscode.markdown.preview.editor" ], @@ -40,7 +41,7 @@ } }, "contributes": { - "notebookMarkupRenderers": [ + "notebookRenderer": [ { "id": "markdownItRenderer", "displayName": "Markdown it renderer", @@ -361,7 +362,8 @@ "@types/highlight.js": "10.1.0", "@types/lodash.throttle": "^4.1.3", "@types/markdown-it": "0.0.2", - "@types/node": "^12.19.9", + "@types/node": "14.x", + "@types/vscode-webview": "^1.57.0", "lodash.throttle": "^4.1.1" }, "repository": { diff --git a/extensions/markdown-language-features/preview-src/activeLineMarker.ts b/extensions/markdown-language-features/preview-src/activeLineMarker.ts index 7204bfae4a..75e0b2487b 100644 --- a/extensions/markdown-language-features/preview-src/activeLineMarker.ts +++ b/extensions/markdown-language-features/preview-src/activeLineMarker.ts @@ -31,4 +31,4 @@ export class ActiveLineMarker { } element.className += ' code-active-line'; } -} \ No newline at end of file +} diff --git a/extensions/markdown-language-features/preview-src/events.ts b/extensions/markdown-language-features/preview-src/events.ts index 8cb41f6661..acedd7e026 100644 --- a/extensions/markdown-language-features/preview-src/events.ts +++ b/extensions/markdown-language-features/preview-src/events.ts @@ -9,4 +9,4 @@ export function onceDocumentLoaded(f: () => void) { } else { f(); } -} \ No newline at end of file +} diff --git a/extensions/markdown-language-features/preview-src/index.ts b/extensions/markdown-language-features/preview-src/index.ts index 17a3110a2b..72922ddbd4 100644 --- a/extensions/markdown-language-features/preview-src/index.ts +++ b/extensions/markdown-language-features/preview-src/index.ts @@ -10,8 +10,6 @@ import { getEditorLineNumberForPageOffset, scrollToRevealSourceLine, getLineElem import { getSettings, getData } from './settings'; import throttle = require('lodash.throttle'); -declare let acquireVsCodeApi: any; - let scrollDisabledCount = 0; const marker = new ActiveLineMarker(); const settings = getSettings(); diff --git a/extensions/markdown-language-features/preview-src/loading.ts b/extensions/markdown-language-features/preview-src/loading.ts index 73bbd57cfd..4c833c28ad 100644 --- a/extensions/markdown-language-features/preview-src/loading.ts +++ b/extensions/markdown-language-features/preview-src/loading.ts @@ -41,4 +41,4 @@ export class StyleLoadingMonitor { poster.postMessage('previewStyleLoadError', { unloadedStyles: this.unloadedStyles }); } } -} \ No newline at end of file +} diff --git a/extensions/markdown-language-features/preview-src/pre.ts b/extensions/markdown-language-features/preview-src/pre.ts index 7c314f74b0..04bd5e5e34 100644 --- a/extensions/markdown-language-features/preview-src/pre.ts +++ b/extensions/markdown-language-features/preview-src/pre.ts @@ -14,4 +14,4 @@ declare global { } window.cspAlerter = new CspAlerter(); -window.styleLoadingMonitor = new StyleLoadingMonitor(); \ No newline at end of file +window.styleLoadingMonitor = new StyleLoadingMonitor(); diff --git a/extensions/markdown-language-features/preview-src/tsconfig.json b/extensions/markdown-language-features/preview-src/tsconfig.json index b1bede72c1..62af34c62f 100644 --- a/extensions/markdown-language-features/preview-src/tsconfig.json +++ b/extensions/markdown-language-features/preview-src/tsconfig.json @@ -8,5 +8,10 @@ "DOM", "DOM.Iterable" ] + }, + "typeAcquisition": { + "include": [ + "@types/vscode-webview" + ] } } diff --git a/extensions/markdown-language-features/src/commandManager.ts b/extensions/markdown-language-features/src/commandManager.ts index 966049df92..38d2711535 100644 --- a/extensions/markdown-language-features/src/commandManager.ts +++ b/extensions/markdown-language-features/src/commandManager.ts @@ -35,4 +35,4 @@ export class CommandManager { this.commands.set(id, vscode.commands.registerCommand(id, impl, thisArg)); } -} \ No newline at end of file +} diff --git a/extensions/markdown-language-features/src/commands/index.ts b/extensions/markdown-language-features/src/commands/index.ts index 32ef9f37b0..b5dc2c136d 100644 --- a/extensions/markdown-language-features/src/commands/index.ts +++ b/extensions/markdown-language-features/src/commands/index.ts @@ -3,11 +3,13 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export { OpenDocumentLinkCommand } from './openDocumentLink'; -export { ShowPreviewCommand, ShowPreviewToSideCommand, ShowLockedPreviewToSideCommand } from './showPreview'; -export { ShowSourceCommand } from './showSource'; -export { RefreshPreviewCommand } from './refreshPreview'; -export { ShowPreviewSecuritySelectorCommand } from './showPreviewSecuritySelector'; export { MoveCursorToPositionCommand } from './moveCursorToPosition'; -export { ToggleLockCommand } from './toggleLock'; +export { OpenDocumentLinkCommand } from './openDocumentLink'; +export { RefreshPreviewCommand } from './refreshPreview'; +export { ReloadPlugins } from './reloadPlugins'; export { RenderDocument } from './renderDocument'; +export { ShowLockedPreviewToSideCommand, ShowPreviewCommand, ShowPreviewToSideCommand } from './showPreview'; +export { ShowPreviewSecuritySelectorCommand } from './showPreviewSecuritySelector'; +export { ShowSourceCommand } from './showSource'; +export { ToggleLockCommand } from './toggleLock'; + diff --git a/extensions/markdown-language-features/src/commands/refreshPreview.ts b/extensions/markdown-language-features/src/commands/refreshPreview.ts index 4e683bd3f2..0b134caee7 100644 --- a/extensions/markdown-language-features/src/commands/refreshPreview.ts +++ b/extensions/markdown-language-features/src/commands/refreshPreview.ts @@ -19,4 +19,4 @@ export class RefreshPreviewCommand implements Command { this.engine.cleanCache(); this.webviewManager.refresh(); } -} \ No newline at end of file +} diff --git a/extensions/markdown-language-features/src/commands/reloadPlugins.ts b/extensions/markdown-language-features/src/commands/reloadPlugins.ts new file mode 100644 index 0000000000..d6bf7a2cbb --- /dev/null +++ b/extensions/markdown-language-features/src/commands/reloadPlugins.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 { Command } from '../commandManager'; +import { MarkdownPreviewManager } from '../features/previewManager'; +import { MarkdownEngine } from '../markdownEngine'; + +export class ReloadPlugins implements Command { + public readonly id = 'markdown.api.reloadPlugins'; + + public constructor( + private readonly webviewManager: MarkdownPreviewManager, + private readonly engine: MarkdownEngine, + ) { } + + public execute(): void { + this.engine.reloadPlugins(); + this.engine.cleanCache(); + this.webviewManager.refresh(); + } +} diff --git a/extensions/markdown-language-features/src/commands/showPreviewSecuritySelector.ts b/extensions/markdown-language-features/src/commands/showPreviewSecuritySelector.ts index 22442b7d74..22fa951e98 100644 --- a/extensions/markdown-language-features/src/commands/showPreviewSecuritySelector.ts +++ b/extensions/markdown-language-features/src/commands/showPreviewSecuritySelector.ts @@ -27,4 +27,4 @@ export class ShowPreviewSecuritySelectorCommand implements Command { this.previewSecuritySelector.showSecuritySelectorForResource(vscode.window.activeTextEditor.document.uri); } } -} \ No newline at end of file +} diff --git a/extensions/markdown-language-features/src/commands/showSource.ts b/extensions/markdown-language-features/src/commands/showSource.ts index 18c4755efe..6e4beb43fa 100644 --- a/extensions/markdown-language-features/src/commands/showSource.ts +++ b/extensions/markdown-language-features/src/commands/showSource.ts @@ -23,4 +23,4 @@ export class ShowSourceCommand implements Command { } return undefined; } -} \ No newline at end of file +} diff --git a/extensions/markdown-language-features/src/commands/toggleLock.ts b/extensions/markdown-language-features/src/commands/toggleLock.ts index 98a6044b13..93f4e767db 100644 --- a/extensions/markdown-language-features/src/commands/toggleLock.ts +++ b/extensions/markdown-language-features/src/commands/toggleLock.ts @@ -16,4 +16,4 @@ export class ToggleLockCommand implements Command { public execute() { this.previewManager.toggleLock(); } -} \ No newline at end of file +} diff --git a/extensions/markdown-language-features/src/extension.ts b/extensions/markdown-language-features/src/extension.ts index c695d5fb66..c9c0b43868 100644 --- a/extensions/markdown-language-features/src/extension.ts +++ b/extensions/markdown-language-features/src/extension.ts @@ -52,12 +52,7 @@ function registerMarkdownLanguageFeatures( ): vscode.Disposable { const selector: vscode.DocumentSelector = { language: 'markdown', scheme: '*' }; - const charPattern = '(\\p{Alphabetic}|\\p{Number}|\\p{Nonspacing_Mark})'; - return vscode.Disposable.from( - vscode.languages.setLanguageConfiguration('markdown', { - wordPattern: new RegExp(`${charPattern}((${charPattern}|[_])?${charPattern})*`, 'ug'), - }), vscode.languages.registerDocumentSymbolProvider(selector, symbolProvider), vscode.languages.registerDocumentLinkProvider(selector, new LinkProvider()), vscode.languages.registerFoldingRangeProvider(selector, new MarkdownFoldingProvider(engine)), @@ -85,5 +80,6 @@ function registerMarkdownCommands( commandManager.register(new commands.OpenDocumentLinkCommand(engine)); commandManager.register(new commands.ToggleLockCommand(previewManager)); commandManager.register(new commands.RenderDocument(engine)); + commandManager.register(new commands.ReloadPlugins(previewManager, engine)); return commandManager; } diff --git a/extensions/markdown-language-features/src/features/documentSymbolProvider.ts b/extensions/markdown-language-features/src/features/documentSymbolProvider.ts index ee8638c503..b8e03e2acd 100644 --- a/extensions/markdown-language-features/src/features/documentSymbolProvider.ts +++ b/extensions/markdown-language-features/src/features/documentSymbolProvider.ts @@ -72,4 +72,4 @@ export default class MDDocumentSymbolProvider implements vscode.DocumentSymbolPr private getSymbolName(entry: TocEntry): string { return '#'.repeat(entry.level) + ' ' + entry.text; } -} \ No newline at end of file +} diff --git a/extensions/markdown-language-features/src/features/preview.ts b/extensions/markdown-language-features/src/features/preview.ts index 9aba08b2b2..97943f6e5e 100644 --- a/extensions/markdown-language-features/src/features/preview.ts +++ b/extensions/markdown-language-features/src/features/preview.ts @@ -11,8 +11,8 @@ import { Logger } from '../logger'; import { MarkdownContributionProvider } from '../markdownExtensions'; import { Disposable } from '../util/dispose'; import { isMarkdownFile } from '../util/file'; -import { normalizeResource, WebviewResourceProvider } from '../util/resources'; -import { getVisibleLine, TopmostLineMonitor } from '../util/topmostLineMonitor'; +import { WebviewResourceProvider } from '../util/resources'; +import { getVisibleLine, LastScrollLocation, TopmostLineMonitor } from '../util/topmostLineMonitor'; import { MarkdownPreviewConfigurationManager } from './previewConfig'; import { MarkdownContentProvider, MarkdownContentProviderOutput } from './previewContentProvider'; import { MarkdownEngine } from '../markdownEngine'; @@ -120,6 +120,8 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { private imageInfo: { readonly id: string, readonly width: number, readonly height: number; }[] = []; private readonly _fileWatchersBySrc = new Map(); + private readonly _onScrollEmitter = this._register(new vscode.EventEmitter()); + public readonly onScroll = this._onScrollEmitter.event; constructor( webview: vscode.WebviewPanel, @@ -324,7 +326,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { private onDidScrollPreview(line: number) { this.line = line; - + this._onScrollEmitter.fire({ line: this.line, uri: this._resource }); const config = this._previewConfigurations.loadAndCacheConfiguration(this._resource); if (!config.scrollEditorWithPreview) { return; @@ -336,13 +338,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { } this.isScrolling = true; - const sourceLine = Math.floor(line); - const fraction = line - sourceLine; - const text = editor.document.lineAt(sourceLine).text; - const start = Math.floor(fraction * text.length); - editor.revealRange( - new vscode.Range(sourceLine, start, sourceLine + 1, 0), - vscode.TextEditorRevealType.AtTop); + scrollEditorToLine(line, editor); } } @@ -427,12 +423,12 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { baseRoots.push(vscode.Uri.file(path.dirname(this._resource.fsPath))); } - return baseRoots.map(root => normalizeResource(this._resource, root)); + return baseRoots; } private async onDidClickPreviewLink(href: string) { - let [hrefPath, fragment] = decodeURIComponent(href).split('#'); + let [hrefPath, fragment] = href.split('#').map(c => decodeURIComponent(c)); if (hrefPath[0] !== '/') { // We perviously already resolve absolute paths. @@ -460,7 +456,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { //#region WebviewResourceProvider asWebviewUri(resource: vscode.Uri) { - return this._webviewPanel.webview.asWebviewUri(normalizeResource(this._resource, resource)); + return this._webviewPanel.webview.asWebviewUri(resource); } get cspSource() { @@ -497,11 +493,13 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown webview: vscode.WebviewPanel, contentProvider: MarkdownContentProvider, previewConfigurations: MarkdownPreviewConfigurationManager, + topmostLineMonitor: TopmostLineMonitor, logger: Logger, contributionProvider: MarkdownContributionProvider, engine: MarkdownEngine, + scrollLine?: number, ): StaticMarkdownPreview { - return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, logger, contributionProvider, engine); + return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, topmostLineMonitor, logger, contributionProvider, engine, scrollLine); } private readonly preview: MarkdownPreview; @@ -511,13 +509,15 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown resource: vscode.Uri, contentProvider: MarkdownContentProvider, private readonly _previewConfigurations: MarkdownPreviewConfigurationManager, + topmostLineMonitor: TopmostLineMonitor, logger: Logger, contributionProvider: MarkdownContributionProvider, engine: MarkdownEngine, + scrollLine?: number, ) { super(); - - this.preview = this._register(new MarkdownPreview(this._webviewPanel, resource, undefined, { + const topScrollLocation = scrollLine ? new StartingScrollLine(scrollLine) : undefined; + this.preview = this._register(new MarkdownPreview(this._webviewPanel, resource, topScrollLocation, { getAdditionalState: () => { return {}; }, openPreviewLinkToMarkdownFile: () => { /* todo */ } }, engine, contentProvider, _previewConfigurations, logger, contributionProvider)); @@ -529,6 +529,16 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown this._register(this._webviewPanel.onDidChangeViewState(e => { this._onDidChangeViewState.fire(e); })); + + this._register(this.preview.onScroll((scrollInfo) => { + topmostLineMonitor.setPreviousEditorLine(scrollInfo); + })); + + this._register(topmostLineMonitor.onDidChanged(event => { + if (this.preview.isPreviewOf(event.resource)) { + this.preview.scrollTo(event.line); + } + })); } private readonly _onDispose = this._register(new vscode.EventEmitter()); @@ -789,3 +799,18 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow } } +/** + * Change the top-most visible line of `editor` to be at `line` + */ +export function scrollEditorToLine( + line: number, + editor: vscode.TextEditor +) { + const sourceLine = Math.floor(line); + const fraction = line - sourceLine; + const text = editor.document.lineAt(sourceLine).text; + const start = Math.floor(fraction * text.length); + editor.revealRange( + new vscode.Range(sourceLine, start, sourceLine + 1, 0), + vscode.TextEditorRevealType.AtTop); +} diff --git a/extensions/markdown-language-features/src/features/previewConfig.ts b/extensions/markdown-language-features/src/features/previewConfig.ts index c277508f83..7f8ac1afe1 100644 --- a/extensions/markdown-language-features/src/features/previewConfig.ts +++ b/extensions/markdown-language-features/src/features/previewConfig.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { equals } from '../util/arrays'; export class MarkdownPreviewConfiguration { public static getForResource(resource: vscode.Uri) { @@ -21,7 +22,7 @@ export class MarkdownPreviewConfiguration { public readonly lineHeight: number; public readonly fontSize: number; public readonly fontFamily: string | undefined; - public readonly styles: string[]; + public readonly styles: readonly string[]; private constructor(resource: vscode.Uri) { const editorConfig = vscode.workspace.getConfiguration('editor', resource); @@ -49,7 +50,7 @@ export class MarkdownPreviewConfiguration { } public isEqualTo(otherConfig: MarkdownPreviewConfiguration) { - for (let key in this) { + for (const key in this) { if (this.hasOwnProperty(key) && key !== 'styles') { if (this[key] !== otherConfig[key]) { return false; @@ -57,17 +58,7 @@ export class MarkdownPreviewConfiguration { } } - // Check styles - if (this.styles.length !== otherConfig.styles.length) { - return false; - } - for (let i = 0; i < this.styles.length; ++i) { - if (this.styles[i] !== otherConfig.styles[i]) { - return false; - } - } - - return true; + return equals(this.styles, otherConfig.styles); } [key: string]: any; diff --git a/extensions/markdown-language-features/src/features/previewContentProvider.ts b/extensions/markdown-language-features/src/features/previewContentProvider.ts index 1c10c2e226..3fe26d4a0b 100644 --- a/extensions/markdown-language-features/src/features/previewContentProvider.ts +++ b/extensions/markdown-language-features/src/features/previewContentProvider.ts @@ -81,7 +81,7 @@ export class MarkdownContentProvider { const nonce = new Date().getTime() + '' + new Date().getMilliseconds(); const csp = this.getCsp(resourceProvider, sourceUri, nonce); - const body = await this.engine.render(markdownDocument); + const body = await this.engine.render(markdownDocument, resourceProvider); const html = ` diff --git a/extensions/markdown-language-features/src/features/previewManager.ts b/extensions/markdown-language-features/src/features/previewManager.ts index f5a889292b..e335423d73 100644 --- a/extensions/markdown-language-features/src/features/previewManager.ts +++ b/extensions/markdown-language-features/src/features/previewManager.ts @@ -9,9 +9,10 @@ import { MarkdownEngine } from '../markdownEngine'; import { MarkdownContributionProvider } from '../markdownExtensions'; import { Disposable, disposeAll } from '../util/dispose'; import { TopmostLineMonitor } from '../util/topmostLineMonitor'; -import { DynamicMarkdownPreview, ManagedMarkdownPreview, StartingScrollFragment, StaticMarkdownPreview } from './preview'; +import { DynamicMarkdownPreview, ManagedMarkdownPreview, StartingScrollFragment, StaticMarkdownPreview, scrollEditorToLine } from './preview'; import { MarkdownPreviewConfigurationManager } from './previewConfig'; import { MarkdownContentProvider } from './previewContentProvider'; +import { isMarkdownFile } from '../util/file'; export interface DynamicPreviewSettings { readonly resourceColumn: vscode.ViewColumn; @@ -75,6 +76,17 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview super(); this._register(vscode.window.registerWebviewPanelSerializer(DynamicMarkdownPreview.viewType, this)); this._register(vscode.window.registerCustomEditorProvider(this.customEditorViewType, this)); + + this._register(vscode.window.onDidChangeActiveTextEditor(textEditor => { + + // When at a markdown file, apply existing scroll settings + if (textEditor && textEditor.document && isMarkdownFile(textEditor.document)) { + const line = this._topmostLineMonitor.getPreviousEditorLineByUri(textEditor.document.uri); + if (line) { + scrollEditorToLine(line, textEditor); + } + } + })); } public refresh() { @@ -160,14 +172,18 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview document: vscode.TextDocument, webview: vscode.WebviewPanel ): Promise { + const lineNumber = this._topmostLineMonitor.getPreviousEditorLineByUri(document.uri); const preview = StaticMarkdownPreview.revive( document.uri, webview, this._contentProvider, this._previewConfigurations, + this._topmostLineMonitor, this._logger, this._contributions, - this._engine); + this._engine, + lineNumber + ); this.registerStaticPreview(preview); } @@ -175,11 +191,14 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview resource: vscode.Uri, previewSettings: DynamicPreviewSettings ): DynamicMarkdownPreview { + const activeTextEditorURI = vscode.window.activeTextEditor?.document.uri; + const scrollLine = (activeTextEditorURI?.toString() === resource.toString()) ? vscode.window.activeTextEditor?.visibleRanges[0].start.line : undefined; const preview = DynamicMarkdownPreview.create( { resource, resourceColumn: previewSettings.resourceColumn, locked: previewSettings.locked, + line: scrollLine, }, previewSettings.previewColumn, this._contentProvider, diff --git a/extensions/markdown-language-features/src/markdownEngine.ts b/extensions/markdown-language-features/src/markdownEngine.ts index 0c0c83a1be..e290143105 100644 --- a/extensions/markdown-language-features/src/markdownEngine.ts +++ b/extensions/markdown-language-features/src/markdownEngine.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { MarkdownIt, Token } from 'markdown-it'; -import * as path from 'path'; import * as vscode from 'vscode'; import { MarkdownContributionProvider as MarkdownContributionProvider } from './markdownExtensions'; import { Slugifier } from './slugify'; import { SkinnyTextDocument } from './tableOfContentsProvider'; import { hash } from './util/hash'; -import { isOfScheme, MarkdownFileExtensions, Schemes } from './util/links'; +import { isOfScheme, Schemes } from './util/links'; +import { WebviewResourceProvider } from './util/resources'; const UNICODE_NEWLINE_REGEX = /\u2028|\u2029/g; @@ -62,12 +62,14 @@ export interface RenderOutput { interface RenderEnv { containingImages: { src: string }[]; + currentDocument: vscode.Uri | undefined; + resourceProvider: WebviewResourceProvider | undefined; } export class MarkdownEngine { + private md?: Promise; - private currentDocument?: vscode.Uri; private _slugCount = new Map(); private _tokenCache = new TokenCache(); @@ -113,7 +115,7 @@ export class MarkdownEngine { this.addLineNumberRenderer(md, renderName); } - this.addImageStabilizer(md); + this.addImageRenderer(md); this.addFencedRenderer(md); this.addLinkNormalizer(md); this.addLinkValidator(md); @@ -128,6 +130,10 @@ export class MarkdownEngine { return md; } + public reloadPlugins() { + this.md = undefined; + } + private tokenizeDocument( document: SkinnyTextDocument, config: MarkdownItConfig, @@ -138,27 +144,18 @@ export class MarkdownEngine { return cached; } - this.currentDocument = document.uri; - const tokens = this.tokenizeString(document.getText(), engine); this._tokenCache.update(document, config, tokens); return tokens; } - public async renderText(document: vscode.Uri, text: string): Promise { // {{SQL CARBON EDIT}} - Add renderText method - const config = this.getConfig(document); - const engine = await this.getEngine(config); - this.currentDocument = document; - return engine.render(text, config); - } - private tokenizeString(text: string, engine: MarkdownIt) { this._slugCount = new Map(); return engine.parse(text.replace(UNICODE_NEWLINE_REGEX, ''), {}); } - public async render(input: SkinnyTextDocument | string): Promise { + public async render(input: SkinnyTextDocument | string, resourceProvider?: WebviewResourceProvider): Promise { const config = this.getConfig(typeof input === 'string' ? undefined : input.uri); const engine = await this.getEngine(config); @@ -167,7 +164,9 @@ export class MarkdownEngine { : this.tokenizeDocument(input, config, engine); const env: RenderEnv = { - containingImages: [] + containingImages: [], + currentDocument: typeof input === 'string' ? undefined : input.uri, + resourceProvider, }; const html = engine.renderer.render(tokens, { @@ -200,12 +199,12 @@ export class MarkdownEngine { }; } - private addLineNumberRenderer(md: any, ruleName: string): void { + private addLineNumberRenderer(md: MarkdownIt, ruleName: string): void { const original = md.renderer.rules[ruleName]; - md.renderer.rules[ruleName] = (tokens: any, idx: number, options: any, env: any, self: any) => { + md.renderer.rules[ruleName] = (tokens: Token[], idx: number, options: any, env: any, self: any) => { const token = tokens[idx]; if (token.map && token.map.length) { - token.attrSet('data-line', token.map[0]); + token.attrSet('data-line', token.map[0] + ''); token.attrJoin('class', 'code-line'); } @@ -217,9 +216,9 @@ export class MarkdownEngine { }; } - private addImageStabilizer(md: any): void { + private addImageRenderer(md: MarkdownIt): void { const original = md.renderer.rules.image; - md.renderer.rules.image = (tokens: any, idx: number, options: any, env: RenderEnv, self: any) => { + md.renderer.rules.image = (tokens: Token[], idx: number, options: any, env: RenderEnv, self: any) => { const token = tokens[idx]; token.attrJoin('class', 'loading'); @@ -228,6 +227,11 @@ export class MarkdownEngine { env.containingImages?.push({ src }); const imgHash = hash(src); token.attrSet('id', `image-hash-${imgHash}`); + + if (!token.attrGet('data-src')) { + token.attrSet('src', this.toResourceUri(src, env.currentDocument, env.resourceProvider)); + token.attrSet('data-src', src); + } } if (original) { @@ -238,9 +242,9 @@ export class MarkdownEngine { }; } - private addFencedRenderer(md: any): void { + private addFencedRenderer(md: MarkdownIt): void { const original = md.renderer.rules['fenced']; - md.renderer.rules['fenced'] = (tokens: any, idx: number, options: any, env: any, self: any) => { + md.renderer.rules['fenced'] = (tokens: Token[], idx: number, options: any, env: any, self: any) => { const token = tokens[idx]; if (token.map && token.map.length) { token.attrJoin('class', 'hljs'); @@ -250,7 +254,7 @@ export class MarkdownEngine { }; } - private addLinkNormalizer(md: any): void { + private addLinkNormalizer(md: MarkdownIt): void { const normalizeLink = md.normalizeLink; md.normalizeLink = (link: string) => { try { @@ -259,43 +263,6 @@ 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 - let uri = vscode.Uri.parse('markdown-link:' + link); - - // Relative paths should be resolved correctly inside the preview but we need to - // handle absolute paths specially (for images) to resolve them relative to the workspace root - if (uri.path[0] === '/') { - const root = vscode.workspace.getWorkspaceFolder(this.currentDocument!); - if (root) { - 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)); - } - } - - const extname = path.extname(uri.fsPath); - - if (uri.fragment && (extname === '' || MarkdownFileExtensions.includes(extname))) { - uri = uri.with({ - fragment: this.slugifier.fromHeading(uri.fragment).value - }); - } - return normalizeLink(uri.toString(true).replace(/^markdown-link:/, '')); - } } catch (e) { // noop } @@ -303,7 +270,7 @@ export class MarkdownEngine { }; } - private addLinkValidator(md: any): void { + private addLinkValidator(md: MarkdownIt): void { const validateLink = md.validateLink; md.validateLink = (link: string) => { return validateLink(link) @@ -313,9 +280,9 @@ export class MarkdownEngine { }; } - private addNamedHeaders(md: any): void { + private addNamedHeaders(md: MarkdownIt): void { const original = md.renderer.rules.heading_open; - md.renderer.rules.heading_open = (tokens: any, idx: number, options: any, env: any, self: any) => { + md.renderer.rules.heading_open = (tokens: Token[], idx: number, options: any, env: any, self: any) => { const title = tokens[idx + 1].children.reduce((acc: string, t: any) => acc + t.content, ''); let slug = this.slugifier.fromHeading(title); @@ -338,12 +305,12 @@ export class MarkdownEngine { }; } - private addLinkRenderer(md: any): void { - const old_render = md.renderer.rules.link_open || ((tokens: any, idx: number, options: any, _env: any, self: any) => { + private addLinkRenderer(md: MarkdownIt): void { + const old_render = md.renderer.rules.link_open || ((tokens: Token[], idx: number, options: any, _env: any, self: any) => { return self.renderToken(tokens, idx, options); }); - md.renderer.rules.link_open = (tokens: any, idx: number, options: any, env: any, self: any) => { + md.renderer.rules.link_open = (tokens: Token[], idx: number, options: any, env: any, self: any) => { const token = tokens[idx]; const hrefIndex = token.attrIndex('href'); if (hrefIndex >= 0) { @@ -353,6 +320,50 @@ export class MarkdownEngine { return old_render(tokens, idx, options, env, self); }; } + + private toResourceUri(href: string, currentDocument: vscode.Uri | undefined, resourceProvider: WebviewResourceProvider | undefined): string { + try { + // Support file:// links + if (isOfScheme(Schemes.file, href)) { + const uri = vscode.Uri.parse(href); + if (resourceProvider) { + return resourceProvider.asWebviewUri(uri).toString(true); + } + // Not sure how to resolve this + return href; + } + + // 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(href)) { + // Use a fake scheme for parsing + let uri = vscode.Uri.parse('markdown-link:' + href); + + // Relative paths should be resolved correctly inside the preview but we need to + // handle absolute paths specially to resolve them relative to the workspace root + if (uri.path[0] === '/' && currentDocument) { + const root = vscode.workspace.getWorkspaceFolder(currentDocument); + if (root) { + uri = vscode.Uri.joinPath(root.uri, uri.fsPath).with({ + fragment: uri.fragment, + query: uri.query, + }); + + if (resourceProvider) { + return resourceProvider.asWebviewUri(uri).toString(true); + } else { + uri = uri.with({ scheme: 'markdown-link' }); + } + } + } + + return uri.toString(true).replace(/^markdown-link:/, ''); + } + + return href; + } catch { + return href; + } + } } async function getMarkdownOptions(md: () => MarkdownIt) { diff --git a/extensions/markdown-language-features/src/test/engine.test.ts b/extensions/markdown-language-features/src/test/engine.test.ts index e4964dea86..94b74d914f 100644 --- a/extensions/markdown-language-features/src/test/engine.test.ts +++ b/extensions/markdown-language-features/src/test/engine.test.ts @@ -37,11 +37,11 @@ suite('markdown.engine', () => { 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/workspaceSymbolProvider.test.ts b/extensions/markdown-language-features/src/test/workspaceSymbolProvider.test.ts index 8be325abfd..2d118499f8 100644 --- a/extensions/markdown-language-features/src/test/workspaceSymbolProvider.test.ts +++ b/extensions/markdown-language-features/src/test/workspaceSymbolProvider.test.ts @@ -18,7 +18,7 @@ suite('markdown.WorkspaceSymbolProvider', () => { test('Should not return anything for empty workspace', async () => { const provider = new MarkdownWorkspaceSymbolProvider(symbolProvider, new InMemoryWorkspaceMarkdownDocumentProvider([])); - assert.deepEqual(await provider.provideWorkspaceSymbols(''), []); + assert.deepStrictEqual(await provider.provideWorkspaceSymbols(''), []); }); test('Should return symbols from workspace with one markdown file', async () => { diff --git a/extensions/markdown-language-features/src/util/arrays.ts b/extensions/markdown-language-features/src/util/arrays.ts index 9ba9df12a9..bf5524c901 100644 --- a/extensions/markdown-language-features/src/util/arrays.ts +++ b/extensions/markdown-language-features/src/util/arrays.ts @@ -15,4 +15,4 @@ export function equals(one: ReadonlyArray, other: ReadonlyArray, itemEq } return true; -} \ No newline at end of file +} diff --git a/extensions/markdown-language-features/src/util/dispose.ts b/extensions/markdown-language-features/src/util/dispose.ts index 83ac3bf543..33c2a708d8 100644 --- a/extensions/markdown-language-features/src/util/dispose.ts +++ b/extensions/markdown-language-features/src/util/dispose.ts @@ -39,4 +39,4 @@ export abstract class Disposable { protected get isDisposed() { return this._isDisposed; } -} \ No newline at end of file +} diff --git a/extensions/markdown-language-features/src/util/file.ts b/extensions/markdown-language-features/src/util/file.ts index c8acf43f6e..7ce14bffe0 100644 --- a/extensions/markdown-language-features/src/util/file.ts +++ b/extensions/markdown-language-features/src/util/file.ts @@ -7,4 +7,4 @@ import * as vscode from 'vscode'; export function isMarkdownFile(document: vscode.TextDocument) { return document.languageId === 'markdown'; -} \ No newline at end of file +} diff --git a/extensions/markdown-language-features/src/util/lazy.ts b/extensions/markdown-language-features/src/util/lazy.ts index 270d93de07..47bf8aa929 100644 --- a/extensions/markdown-language-features/src/util/lazy.ts +++ b/extensions/markdown-language-features/src/util/lazy.ts @@ -36,4 +36,4 @@ class LazyValue implements Lazy { export function lazy(getValue: () => T): Lazy { return new LazyValue(getValue); -} \ No newline at end of file +} diff --git a/extensions/markdown-language-features/src/util/links.ts b/extensions/markdown-language-features/src/util/links.ts index 1f74343119..830bb7b96d 100644 --- a/extensions/markdown-language-features/src/util/links.ts +++ b/extensions/markdown-language-features/src/util/links.ts @@ -14,7 +14,6 @@ export const Schemes = { data: 'data:', vscode: 'vscode:', 'vscode-insiders': 'vscode-insiders:', - 'vscode-resource': 'vscode-resource:', }; const knownSchemes = [ diff --git a/extensions/markdown-language-features/src/util/resources.ts b/extensions/markdown-language-features/src/util/resources.ts index f544dd6b5c..8894f58af9 100644 --- a/extensions/markdown-language-features/src/util/resources.ts +++ b/extensions/markdown-language-features/src/util/resources.ts @@ -11,23 +11,3 @@ export interface WebviewResourceProvider { readonly cspSource: string; } -export function normalizeResource( - base: vscode.Uri, - resource: vscode.Uri -): vscode.Uri { - // If we have a windows path and are loading a workspace with an authority, - // make sure we use a unc path with an explicit localhost authority. - // - // Otherwise, the `` rule will insert the authority into the resolved resource - // URI incorrectly. - if (base.authority && !resource.authority) { - const driveMatch = resource.path.match(/^\/(\w):\//); - if (driveMatch) { - return vscode.Uri.file(`\\\\localhost\\${driveMatch[1]}$\\${resource.fsPath.replace(/^\w:\\/, '')}`).with({ - fragment: resource.fragment, - query: resource.query - }); - } - } - return resource; -} diff --git a/extensions/markdown-language-features/src/util/topmostLineMonitor.ts b/extensions/markdown-language-features/src/util/topmostLineMonitor.ts index acf08006cf..abba94ddc6 100644 --- a/extensions/markdown-language-features/src/util/topmostLineMonitor.ts +++ b/extensions/markdown-language-features/src/util/topmostLineMonitor.ts @@ -7,18 +7,32 @@ import * as vscode from 'vscode'; import { Disposable } from '../util/dispose'; import { isMarkdownFile } from './file'; +export interface LastScrollLocation { + readonly line: number; + readonly uri: vscode.Uri; +} + export class TopmostLineMonitor extends Disposable { private readonly pendingUpdates = new Map(); private readonly throttle = 50; + private previousEditorInfo = new Map(); + public isPrevEditorCustom = false; constructor() { super(); + + if (vscode.window.activeTextEditor) { + const line = getVisibleLine(vscode.window.activeTextEditor); + this.setPreviousEditorLine({ uri: vscode.window.activeTextEditor.document.uri, line: line ?? 0 }); + } + this._register(vscode.window.onDidChangeTextEditorVisibleRanges(event => { if (isMarkdownFile(event.textEditor.document)) { const line = getVisibleLine(event.textEditor); if (typeof line === 'number') { this.updateLine(event.textEditor.document.uri, line); + this.setPreviousEditorLine({ uri: event.textEditor.document.uri, line: line }); } } })); @@ -27,7 +41,16 @@ export class TopmostLineMonitor extends Disposable { private readonly _onChanged = this._register(new vscode.EventEmitter<{ readonly resource: vscode.Uri, readonly line: number }>()); public readonly onDidChanged = this._onChanged.event; - private updateLine( + public setPreviousEditorLine(scrollLocation: LastScrollLocation): void { + this.previousEditorInfo.set(scrollLocation.uri.toString(), scrollLocation); + } + + public getPreviousEditorLineByUri(resource: vscode.Uri): number | undefined { + const scrollLoc = this.previousEditorInfo.get(resource.toString()); + return scrollLoc?.line; + } + + public updateLine( resource: vscode.Uri, line: number ) { diff --git a/extensions/markdown-language-features/yarn.lock b/extensions/markdown-language-features/yarn.lock index 55c8fe601b..0b4295a56d 100644 --- a/extensions/markdown-language-features/yarn.lock +++ b/extensions/markdown-language-features/yarn.lock @@ -26,10 +26,15 @@ resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.2.tgz#5d9ad19e6e6508cdd2f2596df86fd0aade598660" integrity sha1-XZrRnm5lCM3S8llt+G/Qqt5ZhmA= -"@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== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== + +"@types/vscode-webview@^1.57.0": + version "1.57.0" + resolved "https://registry.yarnpkg.com/@types/vscode-webview/-/vscode-webview-1.57.0.tgz#bad5194d45ae8d03afc1c0f67f71ff5e7a243bbf" + integrity sha512-x3Cb/SMa1IwRHfSvKaZDZOTh4cNoG505c3NjTqGlMC082m++x/ETUmtYniDsw6SSmYzZXO8KBNhYxR0+VqymqA== applicationinsights@1.7.4: version "1.7.4" diff --git a/extensions/notebook-markdown-extensions/.gitignore b/extensions/markdown-math/.gitignore similarity index 100% rename from extensions/notebook-markdown-extensions/.gitignore rename to extensions/markdown-math/.gitignore diff --git a/extensions/notebook-markdown-extensions/.vscodeignore b/extensions/markdown-math/.vscodeignore similarity index 100% rename from extensions/notebook-markdown-extensions/.vscodeignore rename to extensions/markdown-math/.vscodeignore diff --git a/extensions/notebook-markdown-extensions/README.md b/extensions/markdown-math/README.md similarity index 75% rename from extensions/notebook-markdown-extensions/README.md rename to extensions/markdown-math/README.md index cc1d7f6fc4..64d18d01be 100644 --- a/extensions/notebook-markdown-extensions/README.md +++ b/extensions/markdown-math/README.md @@ -1,3 +1,3 @@ -# Markdown Notebook Math support +# Markdown Math support **Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. diff --git a/extensions/notebook-markdown-extensions/esbuild.js b/extensions/markdown-math/esbuild.js similarity index 91% rename from extensions/notebook-markdown-extensions/esbuild.js rename to extensions/markdown-math/esbuild.js index f1bb454f4f..52a04f50df 100644 --- a/extensions/notebook-markdown-extensions/esbuild.js +++ b/extensions/markdown-math/esbuild.js @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ const path = require('path'); const fse = require('fs-extra'); @@ -20,7 +20,6 @@ const outDir = path.join(outputRoot, 'notebook-out'); esbuild.build({ entryPoints: [ path.join(__dirname, 'notebook', 'katex.ts'), - path.join(__dirname, 'notebook', 'emoji.ts') ], bundle: true, minify: true, diff --git a/extensions/markdown-math/extension-browser.webpack.config.js b/extensions/markdown-math/extension-browser.webpack.config.js new file mode 100644 index 0000000000..cc74eb2a15 --- /dev/null +++ b/extensions/markdown-math/extension-browser.webpack.config.js @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +module.exports = withBrowserDefaults({ + context: __dirname, + entry: { + extension: './src/extension.ts' + } +}); diff --git a/extensions/markdown-math/extension.webpack.config.js b/extensions/markdown-math/extension.webpack.config.js new file mode 100644 index 0000000000..f35561d9f2 --- /dev/null +++ b/extensions/markdown-math/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/notebook-markdown-extensions/icon.png b/extensions/markdown-math/icon.png similarity index 100% rename from extensions/notebook-markdown-extensions/icon.png rename to extensions/markdown-math/icon.png diff --git a/extensions/markdown-math/notebook/katex.ts b/extensions/markdown-math/notebook/katex.ts new file mode 100644 index 0000000000..0ac46e89d8 --- /dev/null +++ b/extensions/markdown-math/notebook/katex.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ +import type * as markdownIt from 'markdown-it'; + +const styleHref = import.meta.url.replace(/katex.js$/, 'katex.min.css'); + +export async function activate(ctx: { + getRenderer: (id: string) => Promise +}) { + const markdownItRenderer = await ctx.getRenderer('markdownItRenderer'); + if (!markdownItRenderer) { + throw new Error('Could not load markdownItRenderer'); + } + + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.classList.add('markdown-style'); + link.href = styleHref; + document.head.append(link); + + const style = document.createElement('style'); + style.classList.add('markdown-style'); + style.textContent = ` + .katex-error { + color: var(--vscode-editorError-foreground); + } + `; + document.head.append(style); + + const katex = require('@iktakahiro/markdown-it-katex'); + markdownItRenderer.extendMarkdownIt((md: markdownIt.MarkdownIt) => { + return md.use(katex); + }); +} diff --git a/extensions/notebook-markdown-extensions/notebook/tsconfig.json b/extensions/markdown-math/notebook/tsconfig.json similarity index 100% rename from extensions/notebook-markdown-extensions/notebook/tsconfig.json rename to extensions/markdown-math/notebook/tsconfig.json diff --git a/extensions/markdown-math/package.json b/extensions/markdown-math/package.json new file mode 100644 index 0000000000..98c1c6e455 --- /dev/null +++ b/extensions/markdown-math/package.json @@ -0,0 +1,69 @@ +{ + "name": "markdown-math", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "icon": "icon.png", + "publisher": "vscode", + "license": "MIT", + "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", + "engines": { + "vscode": "^1.54.0" + }, + "categories": [ + "Other" + ], + "capabilities": { + "virtualWorkspaces": true, + "untrustedWorkspaces": { + "supported": true + } + }, + "main": "./out/extension", + "browser": "./dist/browser/extension", + "activationEvents": [], + "contributes": { + "notebookRenderer": [ + { + "id": "markdownItRenderer-katex", + "displayName": "Markdown it KaTeX renderer", + "entrypoint": { + "extends": "markdownItRenderer", + "path": "./notebook-out/katex.js" + } + } + ], + "markdown.markdownItPlugins": true, + "markdown.previewStyles": [ + "./node_modules/katex/dist/katex.min.css", + "./preview-styles/index.css" + ], + "configuration": [ + { + "title": "Markdown", + "properties": { + "markdown.math.enabled": { + "type": "boolean", + "default": true, + "description": "%config.markdown.math.enabled%" + } + } + } + ] + }, + "scripts": { + "compile": "npm run build-notebook", + "watch": "npm run build-notebook", + "build-notebook": "node ./esbuild" + }, + "dependencies": { + "@iktakahiro/markdown-it-katex": "https://github.com/mjbvz/markdown-it-katex.git" + }, + "devDependencies": { + "@types/markdown-it": "^0.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } +} diff --git a/extensions/markdown-math/package.nls.json b/extensions/markdown-math/package.nls.json new file mode 100644 index 0000000000..5fb6a52005 --- /dev/null +++ b/extensions/markdown-math/package.nls.json @@ -0,0 +1,5 @@ +{ + "displayName": "Markdown Math", + "description": "Adds math support to markdown in notebooks.", + "config.markdown.math.enabled": "Enable/disable rendering math in the built-in markdown preview." +} diff --git a/extensions/notebook-markdown-extensions/notebook/emoji.ts b/extensions/markdown-math/preview-styles/index.css similarity index 54% rename from extensions/notebook-markdown-extensions/notebook/emoji.ts rename to extensions/markdown-math/preview-styles/index.css index bf82f98ba0..dd205f5905 100644 --- a/extensions/notebook-markdown-extensions/notebook/emoji.ts +++ b/extensions/markdown-math/preview-styles/index.css @@ -1,11 +1,8 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type * as markdownIt from 'markdown-it'; -const emoji = require('markdown-it-emoji'); - -export function extendMarkdownIt(md: markdownIt.MarkdownIt) { - return md.use(emoji); +.katex-error { + color: var(--vscode-editorError-foreground); } diff --git a/extensions/markdown-math/src/extension.ts b/extensions/markdown-math/src/extension.ts new file mode 100644 index 0000000000..24a243e50a --- /dev/null +++ b/extensions/markdown-math/src/extension.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; + +const enabledSetting = 'markdown.math.enabled'; + +export function activate(context: vscode.ExtensionContext) { + function isEnabled(): boolean { + const config = vscode.workspace.getConfiguration('markdown'); + console.log(config.get('math.enabled', true)); + return config.get('math.enabled', true); + } + + vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(enabledSetting)) { + vscode.commands.executeCommand('markdown.api.reloadPlugins'); + } + }, undefined, context.subscriptions); + + return { + extendMarkdownIt(md: any) { + if (isEnabled()) { + const katex = require('@iktakahiro/markdown-it-katex'); + return md.use(katex); + } + return md; + } + }; +} diff --git a/extensions/markdown-math/src/types.d.ts b/extensions/markdown-math/src/types.d.ts new file mode 100644 index 0000000000..711cf9c84b --- /dev/null +++ b/extensions/markdown-math/src/types.d.ts @@ -0,0 +1 @@ +/// diff --git a/extensions/markdown-math/tsconfig.json b/extensions/markdown-math/tsconfig.json new file mode 100644 index 0000000000..1decd91e33 --- /dev/null +++ b/extensions/markdown-math/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out", + "experimentalDecorators": true, + "lib": [ + "es6", + "es2015.promise", + "es2019.array", + "es2020.string", + "dom" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/extensions/markdown-math/yarn.lock b/extensions/markdown-math/yarn.lock new file mode 100644 index 0000000000..f402ca890b --- /dev/null +++ b/extensions/markdown-math/yarn.lock @@ -0,0 +1,26 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@iktakahiro/markdown-it-katex@https://github.com/mjbvz/markdown-it-katex.git": + version "4.0.1" + resolved "https://github.com/mjbvz/markdown-it-katex.git#2bf0b89c6c22ef0b585f55ccab66d1f7c5356bea" + dependencies: + katex "^0.13.0" + +"@types/markdown-it@^0.0.0": + version "0.0.0" + resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.0.tgz#8f6acaa5e3245e275f684e95deb3e518d1c6ab16" + integrity sha1-j2rKpeMkXidfaE6V3rPlGNHGqxY= + +commander@^6.0.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + +katex@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/katex/-/katex-0.13.0.tgz#62900e56c1ad8fdf7da23399e50d7a7b690b39ab" + integrity sha512-6cHbzbegYgS9vvVGuH8UA+o97X+ZshtboSqJJCdq7trBYzuD75JNwr7Ef606xkUjecPPhFnyB+afx1dVafielg== + dependencies: + commander "^6.0.0" diff --git a/extensions/merge-conflict/package.json b/extensions/merge-conflict/package.json index 74f7b848fa..2c4719f4b7 100644 --- a/extensions/merge-conflict/package.json +++ b/extensions/merge-conflict/package.json @@ -159,7 +159,7 @@ "vscode-nls": "^4.0.0" }, "devDependencies": { - "@types/node": "^12.19.9" + "@types/node": "14.x" }, "repository": { "type": "git", diff --git a/extensions/merge-conflict/src/contentProvider.ts b/extensions/merge-conflict/src/contentProvider.ts index 49bbf0aa94..4c971d8f23 100644 --- a/extensions/merge-conflict/src/contentProvider.ts +++ b/extensions/merge-conflict/src/contentProvider.ts @@ -51,4 +51,4 @@ export default class MergeConflictContentProvider implements vscode.TextDocument return null; } } -} \ No newline at end of file +} diff --git a/extensions/merge-conflict/src/delayer.ts b/extensions/merge-conflict/src/delayer.ts index 47ab41521c..c67cd774c3 100644 --- a/extensions/merge-conflict/src/delayer.ts +++ b/extensions/merge-conflict/src/delayer.ts @@ -76,4 +76,4 @@ export class Delayer { this.timeout = null; } } -} \ No newline at end of file +} diff --git a/extensions/merge-conflict/yarn.lock b/extensions/merge-conflict/yarn.lock index 687f15f481..f7a30098ef 100644 --- a/extensions/merge-conflict/yarn.lock +++ b/extensions/merge-conflict/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@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@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== vscode-nls@^4.0.0: version "4.0.0" diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index 7b47354df0..148b5c3625 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -46,7 +46,7 @@ "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, "devDependencies": { - "@types/node": "^12.19.9", + "@types/node": "14.x", "@types/node-fetch": "^2.5.7", "@types/randombytes": "^2.0.0", "@types/sha.js": "^2.4.0", diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index 1ae1ecb338..73f9375210 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -396,19 +396,19 @@ export class AzureActiveDirectoryService { } private getCallbackEnvironment(callbackUri: vscode.Uri): string { - if (callbackUri.authority.endsWith('.workspaces.github.com') || callbackUri.authority.endsWith('.github.dev')) { - return `${callbackUri.authority},`; + if (callbackUri.scheme !== 'https' && callbackUri.scheme !== 'http') { + return callbackUri.scheme; } switch (callbackUri.authority) { case 'online.visualstudio.com': - return 'vso,'; + return 'vso'; case 'online-ppe.core.vsengsaas.visualstudio.com': - return 'vsoppe,'; + return 'vsoppe'; case 'online.dev.core.vsengsaas.visualstudio.com': - return 'vsodev,'; + return 'vsodev'; default: - return `${callbackUri.scheme},`; + return callbackUri.authority; } } @@ -417,7 +417,7 @@ export class AzureActiveDirectoryService { const nonce = randomBytes(16).toString('base64'); const port = (callbackUri.authority.match(/:([0-9]*)$/) || [])[1] || (callbackUri.scheme === 'https' ? 443 : 80); const callbackEnvironment = this.getCallbackEnvironment(callbackUri); - const state = `${callbackEnvironment}${port},${encodeURIComponent(nonce)},${encodeURIComponent(callbackUri.query)}`; + const state = `${callbackEnvironment},${port},${encodeURIComponent(nonce)},${encodeURIComponent(callbackUri.query)}`; const signInUrl = `${loginEndpointUrl}${tenant}/oauth2/v2.0/authorize`; let uri = vscode.Uri.parse(signInUrl); const codeVerifier = toBase64UrlEncoding(randomBytes(32).toString('base64')); @@ -566,7 +566,8 @@ export class AzureActiveDirectoryService { }); const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints'); - const endpoint = proxyEndpoints && proxyEndpoints['microsoft'] || `${loginEndpointUrl}${tenant}/oauth2/v2.0/token`; + const endpointUrl = proxyEndpoints?.microsoft || loginEndpointUrl; + const endpoint = `${endpointUrl}${tenant}/oauth2/v2.0/token`; const result = await fetch(endpoint, { method: 'POST', @@ -592,16 +593,20 @@ export class AzureActiveDirectoryService { } private async refreshToken(refreshToken: string, scope: string, sessionId: string): Promise { - try { - Logger.info('Refreshing token...'); - const postData = querystring.stringify({ - refresh_token: refreshToken, - client_id: clientId, - grant_type: 'refresh_token', - scope: scope - }); + Logger.info('Refreshing token...'); + const postData = querystring.stringify({ + refresh_token: refreshToken, + client_id: clientId, + grant_type: 'refresh_token', + scope: scope + }); - const result = await fetch(`https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`, { + let result: Response; + try { + const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints'); + const endpointUrl = proxyEndpoints?.microsoft || loginEndpointUrl; + const endpoint = `${endpointUrl}${tenant}/oauth2/v2.0/token`; + result = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', @@ -609,7 +614,12 @@ export class AzureActiveDirectoryService { }, body: postData }); + } catch (e) { + Logger.error('Refreshing token failed'); + throw new Error(REFRESH_NETWORK_FAILURE); + } + try { if (result.ok) { const json = await result.json(); const token = this.getTokenFromResponse(json, scope, sessionId); @@ -617,12 +627,12 @@ export class AzureActiveDirectoryService { Logger.info('Token refresh success'); return token; } else { - Logger.error('Refreshing token failed'); - throw new Error('Refreshing token failed.'); + throw new Error('Bad request.'); } } catch (e) { - Logger.error('Refreshing token failed'); - throw new Error(REFRESH_NETWORK_FAILURE); + vscode.window.showErrorMessage(localize('signOut', "You have been signed out because reading stored authentication information failed.")); + Logger.error(`Refreshing token failed: ${result.statusText}`); + throw new Error('Refreshing token failed'); } } diff --git a/extensions/microsoft-authentication/src/microsoft-authentication.d.ts b/extensions/microsoft-authentication/src/microsoft-authentication.d.ts index 167d789314..67f7b07081 100644 --- a/extensions/microsoft-authentication/src/microsoft-authentication.d.ts +++ b/extensions/microsoft-authentication/src/microsoft-authentication.d.ts @@ -13,4 +13,4 @@ export interface MicrosoftAuthenticationSession extends AuthenticationSession { * The id token. */ idToken?: string; -} \ No newline at end of file +} diff --git a/extensions/microsoft-authentication/tsconfig.json b/extensions/microsoft-authentication/tsconfig.json index 2c2bce4b8a..6dabc0879a 100644 --- a/extensions/microsoft-authentication/tsconfig.json +++ b/extensions/microsoft-authentication/tsconfig.json @@ -1,22 +1,22 @@ { + "extends": "../tsconfig.base.json", "compilerOptions": { "baseUrl": ".", "experimentalDecorators": true, - "forceConsistentCasingInFileNames": true, - "lib": ["es2019"], "module": "commonjs", "moduleResolution": "node", "noFallthroughCasesInSwitch": true, - "noImplicitReturns": true, "noUnusedLocals": false, "outDir": "dist", "resolveJsonModule": true, "rootDir": "src", "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "target": "es2019" + "sourceMap": true }, - "exclude": ["node_modules"], - "include": ["src/**/*"] + "exclude": [ + "node_modules" + ], + "include": [ + "src/**/*" + ] } diff --git a/extensions/microsoft-authentication/yarn.lock b/extensions/microsoft-authentication/yarn.lock index fada1d6e8b..54025e55a2 100644 --- a/extensions/microsoft-authentication/yarn.lock +++ b/extensions/microsoft-authentication/yarn.lock @@ -15,10 +15,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.23.tgz#676fa0883450ed9da0bb24156213636290892806" integrity sha512-Z4U8yDAl5TFkmYsZdFPdjeMa57NOvnaf1tljHzhouaPEp7LCj2JKkejpI1ODviIAQuW4CcQmxkQ77rnLsOOoKw== -"@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@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== "@types/randombytes@^2.0.0": version "2.0.0" diff --git a/extensions/notebook-markdown-extensions/notebook/katex.ts b/extensions/notebook-markdown-extensions/notebook/katex.ts deleted file mode 100644 index f862fd9494..0000000000 --- a/extensions/notebook-markdown-extensions/notebook/katex.ts +++ /dev/null @@ -1,20 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import type * as markdownIt from 'markdown-it'; - -const styleHref = import.meta.url.replace(/katex.js$/, 'katex.min.css'); - -const link = document.createElement('link'); -link.rel = 'stylesheet'; -link.classList.add('markdown-style'); -link.href = styleHref; - -document.head.append(link); - -const katex = require('@iktakahiro/markdown-it-katex'); - -export function extendMarkdownIt(md: markdownIt.MarkdownIt) { - return md.use(katex); -} diff --git a/extensions/notebook-markdown-extensions/package.json b/extensions/notebook-markdown-extensions/package.json deleted file mode 100644 index 1622659825..0000000000 --- a/extensions/notebook-markdown-extensions/package.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "name": "notebook-markdown-extensions", - "displayName": "%displayName%", - "description": "%description%", - "version": "1.0.0", - "icon": "icon.png", - "publisher": "vscode", - "enableProposedApi": true, - "license": "MIT", - "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", - "engines": { - "vscode": "^1.54.0" - }, - "categories": [ - "Other" - ], - "capabilities": { - "virtualWorkspaces": false - }, - "contributes": { - "notebookMarkupRenderers": [ - { - "id": "markdownItRenderer-katex", - "displayName": "Markdown it katex renderer", - "entrypoint": "./notebook-out/katex.js", - "dependsOn": "markdownItRenderer" - }, - { - "id": "markdownItRenderer-emoji", - "displayName": "Markdown it emoji renderer", - "entrypoint": "./notebook-out/emoji.js", - "dependsOn": "markdownItRenderer" - } - ] - }, - "scripts": { - "compile": "npm run build-notebook", - "watch": "npm run build-notebook", - "build-notebook": "node ./esbuild" - }, - "devDependencies": { - "@iktakahiro/markdown-it-katex": "https://github.com/mjbvz/markdown-it-katex.git", - "@types/markdown-it": "^0.0.0", - "markdown-it": "^12.0.4", - "markdown-it-emoji": "^2.0.0" - }, - "repository": { - "type": "git", - "url": "https://github.com/microsoft/vscode.git" - } -} diff --git a/extensions/notebook-markdown-extensions/package.nls.json b/extensions/notebook-markdown-extensions/package.nls.json deleted file mode 100644 index e9ae9594dc..0000000000 --- a/extensions/notebook-markdown-extensions/package.nls.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "displayName": "Markdown Notebook math", - "description": "Provides rich language support for Markdown." -} diff --git a/extensions/notebook-markdown-extensions/yarn.lock b/extensions/notebook-markdown-extensions/yarn.lock deleted file mode 100644 index 5895266735..0000000000 --- a/extensions/notebook-markdown-extensions/yarn.lock +++ /dev/null @@ -1,69 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@iktakahiro/markdown-it-katex@https://github.com/mjbvz/markdown-it-katex.git": - version "4.0.1" - resolved "https://github.com/mjbvz/markdown-it-katex.git#e88925a7cb3fd593a14ed117fb43627c4ba910b6" - dependencies: - katex "^0.13.0" - -"@types/markdown-it@^0.0.0": - version "0.0.0" - resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.0.tgz#8f6acaa5e3245e275f684e95deb3e518d1c6ab16" - integrity sha1-j2rKpeMkXidfaE6V3rPlGNHGqxY= - -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== - -commander@^6.0.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" - integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== - -entities@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" - integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== - -katex@^0.13.0: - version "0.13.0" - resolved "https://registry.yarnpkg.com/katex/-/katex-0.13.0.tgz#62900e56c1ad8fdf7da23399e50d7a7b690b39ab" - integrity sha512-6cHbzbegYgS9vvVGuH8UA+o97X+ZshtboSqJJCdq7trBYzuD75JNwr7Ef606xkUjecPPhFnyB+afx1dVafielg== - dependencies: - commander "^6.0.0" - -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" - -markdown-it-emoji@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/markdown-it-emoji/-/markdown-it-emoji-2.0.0.tgz#3164ad4c009efd946e98274f7562ad611089a231" - integrity sha512-39j7/9vP/CPCKbEI44oV8yoPJTpvfeReTn/COgRhSpNrjWF3PfP/JUxxB0hxV6ynOY8KH8Y8aX9NMDdo6z+6YQ== - -markdown-it@^12.0.4: - version "12.0.4" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.0.4.tgz#eec8247d296327eac3ba9746bdeec9cfcc751e33" - integrity sha512-34RwOXZT8kyuOJy25oJNJoulO8L0bTHYWXcdZBYZqFnjIy3NgjeoM3FmPXIOFQ26/lSHYMr8oc62B6adxXcb3Q== - dependencies: - argparse "^2.0.1" - entities "~2.1.0" - linkify-it "^3.0.1" - mdurl "^1.0.1" - uc.micro "^1.0.5" - -mdurl@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" - integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= - -uc.micro@^1.0.1, uc.micro@^1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" - integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== diff --git a/extensions/package.json b/extensions/package.json index 645ea5c23c..06af3843ae 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -4,7 +4,7 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "4.2.4" + "typescript": "4.3.2" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/extensions/simple-browser/package.json b/extensions/simple-browser/package.json index 5fd030b186..f2af986a04 100644 --- a/extensions/simple-browser/package.json +++ b/extensions/simple-browser/package.json @@ -70,8 +70,9 @@ "vscode-nls": "^4.0.0" }, "devDependencies": { - "vscode-codicons": "^0.0.14", - "@types/node": "^12.11.7" + "@types/node": "14.x", + "@types/vscode-webview": "^1.57.0", + "vscode-codicons": "^0.0.14" }, "repository": { "type": "git", diff --git a/extensions/simple-browser/preview-src/events.ts b/extensions/simple-browser/preview-src/events.ts index 8cb41f6661..acedd7e026 100644 --- a/extensions/simple-browser/preview-src/events.ts +++ b/extensions/simple-browser/preview-src/events.ts @@ -9,4 +9,4 @@ export function onceDocumentLoaded(f: () => void) { } 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 index 42c5777a61..8c27186e9b 100644 --- a/extensions/simple-browser/preview-src/index.ts +++ b/extensions/simple-browser/preview-src/index.ts @@ -5,7 +5,6 @@ import { onceDocumentLoaded } from './events'; -declare let acquireVsCodeApi: any; const vscode = acquireVsCodeApi(); function getSettings() { diff --git a/extensions/simple-browser/yarn.lock b/extensions/simple-browser/yarn.lock index 804f85a68a..eb52a2a4a6 100644 --- a/extensions/simple-browser/yarn.lock +++ b/extensions/simple-browser/yarn.lock @@ -2,10 +2,15 @@ # 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== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== + +"@types/vscode-webview@^1.57.0": + version "1.57.0" + resolved "https://registry.yarnpkg.com/@types/vscode-webview/-/vscode-webview-1.57.0.tgz#bad5194d45ae8d03afc1c0f67f71ff5e7a243bbf" + integrity sha512-x3Cb/SMa1IwRHfSvKaZDZOTh4cNoG505c3NjTqGlMC082m++x/ETUmtYniDsw6SSmYzZXO8KBNhYxR0+VqymqA== applicationinsights@1.7.4: version "1.7.4" diff --git a/extensions/sql/cgmanifest.json b/extensions/sql/cgmanifest.json index 45e7c7f4e7..3f0ac384a4 100644 --- a/extensions/sql/cgmanifest.json +++ b/extensions/sql/cgmanifest.json @@ -4,8 +4,8 @@ "component": { "type": "git", "git": { - "name": "Microsoft/vscode-mssql", - "repositoryUrl": "https://github.com/Microsoft/vscode-mssql", + "name": "microsoft/vscode-mssql", + "repositoryUrl": "https://github.com/microsoft/vscode-mssql", "commitHash": "61ae0eb21ac53883a23e09913a5ae77a59126ff9" } }, diff --git a/extensions/sql/syntaxes/sql.tmLanguage.json b/extensions/sql/syntaxes/sql.tmLanguage.json index a27889c5cd..92e2f6b675 100644 --- a/extensions/sql/syntaxes/sql.tmLanguage.json +++ b/extensions/sql/syntaxes/sql.tmLanguage.json @@ -1,10 +1,10 @@ { "information_for_contributors": [ - "This file has been converted from https://github.com/Microsoft/vscode-mssql/blob/main/syntaxes/SQL.plist", + "This file has been converted from https://github.com/microsoft/vscode-mssql/blob/main/syntaxes/SQL.plist", "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-mssql/commit/61ae0eb21ac53883a23e09913a5ae77a59126ff9", + "version": "https://github.com/microsoft/vscode-mssql/commit/61ae0eb21ac53883a23e09913a5ae77a59126ff9", "name": "SQL", "scopeName": "source.sql", "patterns": [ @@ -516,4 +516,4 @@ ] } } -} \ No newline at end of file +} diff --git a/extensions/theme-abyss/themes/abyss-color-theme.json b/extensions/theme-abyss/themes/abyss-color-theme.json index 3ee582a014..cb41484be4 100644 --- a/extensions/theme-abyss/themes/abyss-color-theme.json +++ b/extensions/theme-abyss/themes/abyss-color-theme.json @@ -363,7 +363,7 @@ // "peekViewTitleDescription.foreground": "", // Ports - "ports.iconRunningProcessforeground": "#80a2c2", + "ports.iconRunningProcessForeground": "#80a2c2", // Editor: Diff "diffEditor.insertedTextBackground": "#31958A55", // "diffEditor.insertedTextBorder": "", diff --git a/extensions/theme-defaults/package.json b/extensions/theme-defaults/package.json index d91ac173eb..3669964e82 100644 --- a/extensions/theme-defaults/package.json +++ b/extensions/theme-defaults/package.json @@ -2,11 +2,15 @@ "name": "theme-defaults", "displayName": "%displayName%", "description": "%description%", - "categories": [ "Themes" ], + "categories": [ + "Themes" + ], "version": "1.0.0", "publisher": "vscode", "license": "MIT", - "engines": { "vscode": "*" }, + "engines": { + "vscode": "*" + }, "contributes": { "themes": [ { @@ -24,7 +28,7 @@ } ], "repository": { - "type": "git", - "url": "https://github.com/microsoft/azuredatastudio.git" + "type": "git", + "url": "https://github.com/microsoft/azuredatastudio.git" } -} \ No newline at end of file +} diff --git a/extensions/theme-defaults/themes/dark_plus.json b/extensions/theme-defaults/themes/dark_plus.json index 4fd8979321..e928b63cfb 100644 --- a/extensions/theme-defaults/themes/dark_plus.json +++ b/extensions/theme-defaults/themes/dark_plus.json @@ -91,7 +91,8 @@ "variable", "meta.definition.variable.name", "support.variable", - "entity.name.variable" + "entity.name.variable", + "constant.other.placeholder", // placeholders in strings ], "settings": { "foreground": "#9CDCFE" diff --git a/extensions/theme-defaults/themes/dark_vs.json b/extensions/theme-defaults/themes/dark_vs.json index 010f74ed87..2b4904c804 100644 --- a/extensions/theme-defaults/themes/dark_vs.json +++ b/extensions/theme-defaults/themes/dark_vs.json @@ -16,7 +16,7 @@ "menu.foreground": "#CCCCCC", "statusBarItem.remoteForeground": "#FFF", "statusBarItem.remoteBackground": "#16825D", - "ports.iconRunningProcessforeground": "#369432", + "ports.iconRunningProcessForeground": "#369432", "sideBarSectionHeader.background": "#0000", "sideBarSectionHeader.border": "#ccc3", "tab.lastPinnedBorder": "#ccc3" diff --git a/extensions/theme-defaults/themes/hc_black.json b/extensions/theme-defaults/themes/hc_black.json index 709108b0c2..7faf68ddab 100644 --- a/extensions/theme-defaults/themes/hc_black.json +++ b/extensions/theme-defaults/themes/hc_black.json @@ -10,7 +10,8 @@ "selection.background": "#008000", "editor.selectionBackground": "#FFFFFF", "statusBarItem.remoteBackground": "#00000000", - "ports.iconRunningProcessforeground": "#FFFFFF" + "ports.iconRunningProcessForeground": "#FFFFFF", + "editorWhitespace.foreground": "#7c7c7c" }, "tokenColors": [ { diff --git a/extensions/theme-defaults/themes/light_plus.json b/extensions/theme-defaults/themes/light_plus.json index cbae5efd22..62f15133ea 100644 --- a/extensions/theme-defaults/themes/light_plus.json +++ b/extensions/theme-defaults/themes/light_plus.json @@ -91,7 +91,9 @@ "variable", "meta.definition.variable.name", "support.variable", - "entity.name.variable" + "entity.name.variable", + "constant.other.placeholder", // placeholders in strings + ], "settings": { "foreground": "#001080" diff --git a/extensions/theme-defaults/themes/light_vs.json b/extensions/theme-defaults/themes/light_vs.json index 084e2d2742..b58b9f8832 100644 --- a/extensions/theme-defaults/themes/light_vs.json +++ b/extensions/theme-defaults/themes/light_vs.json @@ -18,13 +18,14 @@ "settings.numberInputBorder": "#CECECE", "statusBarItem.remoteForeground": "#FFF", "statusBarItem.remoteBackground": "#16825D", - "ports.iconRunningProcessforeground": "#369432", + "ports.iconRunningProcessForeground": "#369432", "sideBarSectionHeader.background": "#0000", "sideBarSectionHeader.border": "#61616130", "tab.lastPinnedBorder": "#61616130", "notebook.cellBorderColor": "#E8E8E8", "notebook.selectedCellBackground": "#c8ddf150", - "statusBarItem.errorBackground": "#c72e0f" + "statusBarItem.errorBackground": "#c72e0f", + "list.focusHighlightForeground": "#9DDDFF" }, "tokenColors": [ { diff --git a/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json b/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json index 0c6f9cc64c..278d188a9c 100644 --- a/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json +++ b/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json @@ -29,7 +29,7 @@ "statusBar.debuggingBackground": "#423523", "statusBar.noFolderBackground": "#423523", "statusBarItem.remoteBackground": "#6e583b", - "ports.iconRunningProcessforeground": "#369432", + "ports.iconRunningProcessForeground": "#369432", "activityBar.background": "#221a0f", "activityBar.foreground": "#d3af86", "sideBar.background": "#362712", diff --git a/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json b/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json index 3d13ab7514..ac92eb6aee 100644 --- a/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json +++ b/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json @@ -16,7 +16,7 @@ "editor.lineHighlightBackground": "#303030", "editorLineNumber.activeForeground": "#949494", "editor.wordHighlightBackground": "#4747a180", - "editor.wordHighlightStrongBackground": "#6767ce80", + "editor.wordHighlightStrongBackground": "#6767ce80", "editorCursor.foreground": "#c07020", "editorWhitespace.foreground": "#505037", "editorIndentGuide.background": "#505037", @@ -33,7 +33,7 @@ "statusBar.noFolderBackground": "#505050", "titleBar.activeBackground": "#505050", "statusBarItem.remoteBackground": "#3655b5", - "ports.iconRunningProcessforeground": "#CCCCCC", + "ports.iconRunningProcessForeground": "#CCCCCC", "activityBar.background": "#353535", "activityBar.foreground": "#ffffff", "activityBarBadge.background": "#3655b5", diff --git a/extensions/theme-monokai/themes/monokai-color-theme.json b/extensions/theme-monokai/themes/monokai-color-theme.json index 6acacfb9a6..2b9b207a5a 100644 --- a/extensions/theme-monokai/themes/monokai-color-theme.json +++ b/extensions/theme-monokai/themes/monokai-color-theme.json @@ -11,8 +11,6 @@ "list.activeSelectionBackground": "#75715E", "quickInputList.focusBackground": "#414339", "dropdown.listBackground": "#1e1f1c", - "settings.textInputBackground": "#32342d", - "settings.numberInputBackground": "#32342d", "list.inactiveSelectionBackground": "#414339", "list.hoverBackground": "#3e3d32", "list.dropBackground": "#414339", @@ -47,12 +45,13 @@ "panelTitle.activeBorder": "#75715E", "panelTitle.inactiveForeground": "#75715E", "panel.border": "#414339", + "settings.focusedRowBackground": "#4143395A", "titleBar.activeBackground": "#1e1f1c", "statusBar.background": "#414339", "statusBar.noFolderBackground": "#414339", "statusBar.debuggingBackground": "#75715E", "statusBarItem.remoteBackground": "#AC6218", - "ports.iconRunningProcessforeground": "#ccccc7", + "ports.iconRunningProcessForeground": "#ccccc7", "activityBar.background": "#272822", "activityBar.foreground": "#f8f8f2", "sideBar.background": "#1e1f1c", diff --git a/extensions/theme-quietlight/themes/quietlight-color-theme.json b/extensions/theme-quietlight/themes/quietlight-color-theme.json index 6e9707e5a5..f3b5437642 100644 --- a/extensions/theme-quietlight/themes/quietlight-color-theme.json +++ b/extensions/theme-quietlight/themes/quietlight-color-theme.json @@ -506,7 +506,7 @@ "statusBar.noFolderBackground": "#705697", "statusBar.debuggingBackground": "#705697", "statusBarItem.remoteBackground": "#4e3c69", - "ports.iconRunningProcessforeground": "#749351", + "ports.iconRunningProcessForeground": "#749351", "activityBar.background": "#EDEDF5", "activityBar.foreground": "#705697", "activityBarBadge.background": "#705697", diff --git a/extensions/theme-red/themes/Red-color-theme.json b/extensions/theme-red/themes/Red-color-theme.json index d0e2d16a41..0bed05a5a4 100644 --- a/extensions/theme-red/themes/Red-color-theme.json +++ b/extensions/theme-red/themes/Red-color-theme.json @@ -10,7 +10,7 @@ "statusBar.background": "#700000", "statusBar.noFolderBackground": "#700000", "statusBarItem.remoteBackground": "#c33", - "ports.iconRunningProcessforeground": "#DB7E58", + "ports.iconRunningProcessForeground": "#DB7E58", "editorGroupHeader.tabsBackground": "#330000", "titleBar.activeBackground": "#770000", "titleBar.inactiveBackground": "#772222", diff --git a/extensions/theme-seti/CONTRIBUTING.md b/extensions/theme-seti/CONTRIBUTING.md index 888829ff00..548a437615 100644 --- a/extensions/theme-seti/CONTRIBUTING.md +++ b/extensions/theme-seti/CONTRIBUTING.md @@ -2,18 +2,26 @@ This is an icon theme that uses the icons from [`seti-ui`](https://github.com/jesseweed/seti-ui). +## Previewing icons + +There is a [`./icons/preview.html`](./icons/preview.html) file that can be opened to see all of the icons included in the theme. +To view this, it needs to be hosted by a web server. The easiest way is to open the file with the `Open with Live Server` command from the [Live Server extension](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer). + + ## Updating icons -There is script that can be used to update icons, [./build/update-icon-theme.js](build/update-icon-theme.js). +- Make a PR against https://github.com/jesseweed/seti-ui` with your icon changes. +- Once accepted there, ping us or make a PR yourself that updates the theme and font here -To run this script, run `npm run update` from the `theme-seti` directory. +To adopt the latest changes from https://github.com/jesseweed/seti-ui: -This can be run in one of two ways: looking at a local copy of `seti-ui` for icons, or getting them straight from GitHub. +- have the main branches of `https://github.com/jesseweed/seti-ui` and `https://github.com/microsoft/vscode` cloned in the same parent folder +- in the `seti-ui` folder, run `npm install` and `npm run prepublishOnly`. This will generate updated icons and fonts. +- in the `vscode/extensions/theme-seti` folder run `npm run update`. This will launch the [icon theme update script](build/update-icon-theme.js) that updates the theme as well as the font based on content in `seti-ui`. +- to test the icon theme, look at the icon preview as described above. +- when done, create a PR with the changes in https://github.com/microsoft/vscode. +Add a screenshot of the preview page to accompany it. -If you want to run it from a local copy of `seti-ui`, first clone [`seti-ui`](https://github.com/jesseweed/seti-ui) to the folder next to your `vscode` repo (from the `theme-seti` directory, `../../`). -Then, inside the `set-ui` directory, run `npm install` followed by `npm run prepublishOnly`. This will generate updated icons. - -If you want to download the icons straight from GitHub, change the `FROM_DISK` variable to `false` inside of `update-icon-theme.js`. ### Languages not shipped with `vscode` @@ -23,10 +31,3 @@ These should match [the file mapping in `seti-ui`](https://github.com/jesseweed/ Please try and keep this list in alphabetical order! Thank you. -## Previewing icons - -There is a [`./icons/preview.html`](./icons/preview.html) file that can be opened to see all of the icons included in the theme. -Note that to view this, it needs to be hosted by a web server. - -When updating icons, it is always a good idea to make sure that they work properly by looking at this page. -When submitting a PR that updates these icons, a screenshot of the preview page should accompany it. diff --git a/extensions/theme-seti/build/update-icon-theme.js b/extensions/theme-seti/build/update-icon-theme.js index e39aec50e4..946fae2e95 100644 --- a/extensions/theme-seti/build/update-icon-theme.js +++ b/extensions/theme-seti/build/update-icon-theme.js @@ -31,6 +31,7 @@ let nonBuiltInLanguages = { // { fileNames, extensions } "ocaml": { extensions: ['ml', 'mli', 'mll', 'mly', 'eliom', 'eliomi'] }, "puppet": { extensions: ['puppet'] }, "r": { extensions: ['r', 'rhistory', 'rprofile', 'rt'] }, + "rescript": { extensions: ['res', 'resi'] }, "sass": { extensions: ['sass'] }, "stylus": { extensions: ['styl'] }, "terraform": { extensions: ['tf', 'tfvars', 'hcl'] }, diff --git a/extensions/theme-seti/cgmanifest.json b/extensions/theme-seti/cgmanifest.json index c13fbcb15b..2bd83f5bea 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": "894c49fa9f5ce43dd4f012173a7bb107346e524e" + "commitHash": "e80b920f4268e5daf1f65183dae33297aa4d0d17" } }, "version": "0.1.0" diff --git a/extensions/theme-seti/icons/seti.woff b/extensions/theme-seti/icons/seti.woff index 928e91b30c12227c806b9be7c01e14df4c47aba8..9add6e060ce388057c6d2a91e88f81b802c9b86b 100644 GIT binary patch delta 18014 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 W(W`XHoKBX#N{&c78vO_2psjsDwSVFO diff --git a/extensions/theme-seti/icons/vs-seti-icon-theme.json b/extensions/theme-seti/icons/vs-seti-icon-theme.json index 3dbab55a8a..73d63da6e0 100644 --- a/extensions/theme-seti/icons/vs-seti-icon-theme.json +++ b/extensions/theme-seti/icons/vs-seti-icon-theme.json @@ -1094,391 +1094,415 @@ "fontCharacter": "\\E077", "fontColor": "#cbcb41" }, - "_python_light": { + "_purescript_light": { "fontCharacter": "\\E078", + "fontColor": "#bfc2c1" + }, + "_purescript": { + "fontCharacter": "\\E078", + "fontColor": "#d4d7d6" + }, + "_python_light": { + "fontCharacter": "\\E079", "fontColor": "#498ba7" }, "_python": { - "fontCharacter": "\\E078", + "fontCharacter": "\\E079", "fontColor": "#519aba" }, "_react_light": { - "fontCharacter": "\\E07A", + "fontCharacter": "\\E07B", "fontColor": "#498ba7" }, "_react": { - "fontCharacter": "\\E07A", + "fontCharacter": "\\E07B", "fontColor": "#519aba" }, "_react_1_light": { - "fontCharacter": "\\E07A", + "fontCharacter": "\\E07B", "fontColor": "#cc6d2e" }, "_react_1": { - "fontCharacter": "\\E07A", + "fontCharacter": "\\E07B", "fontColor": "#e37933" }, "_react_2_light": { - "fontCharacter": "\\E07A", + "fontCharacter": "\\E07B", "fontColor": "#b7b73b" }, "_react_2": { - "fontCharacter": "\\E07A", + "fontCharacter": "\\E07B", "fontColor": "#cbcb41" }, "_reasonml_light": { - "fontCharacter": "\\E07B", + "fontCharacter": "\\E07C", "fontColor": "#b8383d" }, "_reasonml": { - "fontCharacter": "\\E07B", + "fontCharacter": "\\E07C", "fontColor": "#cc3e44" }, + "_rescript_light": { + "fontCharacter": "\\E07D", + "fontColor": "#b8383d" + }, + "_rescript": { + "fontCharacter": "\\E07D", + "fontColor": "#cc3e44" + }, + "_rescript_1_light": { + "fontCharacter": "\\E07D", + "fontColor": "#dd4b78" + }, + "_rescript_1": { + "fontCharacter": "\\E07D", + "fontColor": "#f55385" + }, "_rollup_light": { - "fontCharacter": "\\E07C", + "fontCharacter": "\\E07E", "fontColor": "#b8383d" }, "_rollup": { - "fontCharacter": "\\E07C", + "fontCharacter": "\\E07E", "fontColor": "#cc3e44" }, "_ruby_light": { - "fontCharacter": "\\E07D", + "fontCharacter": "\\E07F", "fontColor": "#b8383d" }, "_ruby": { - "fontCharacter": "\\E07D", + "fontCharacter": "\\E07F", "fontColor": "#cc3e44" }, "_rust_light": { - "fontCharacter": "\\E07E", + "fontCharacter": "\\E080", "fontColor": "#627379" }, "_rust": { - "fontCharacter": "\\E07E", + "fontCharacter": "\\E080", "fontColor": "#6d8086" }, "_salesforce_light": { - "fontCharacter": "\\E07F", + "fontCharacter": "\\E081", "fontColor": "#498ba7" }, "_salesforce": { - "fontCharacter": "\\E07F", + "fontCharacter": "\\E081", "fontColor": "#519aba" }, "_sass_light": { - "fontCharacter": "\\E080", + "fontCharacter": "\\E082", "fontColor": "#dd4b78" }, "_sass": { - "fontCharacter": "\\E080", + "fontCharacter": "\\E082", "fontColor": "#f55385" }, "_sbt_light": { - "fontCharacter": "\\E081", + "fontCharacter": "\\E083", "fontColor": "#498ba7" }, "_sbt": { - "fontCharacter": "\\E081", + "fontCharacter": "\\E083", "fontColor": "#519aba" }, "_scala_light": { - "fontCharacter": "\\E082", + "fontCharacter": "\\E084", "fontColor": "#b8383d" }, "_scala": { - "fontCharacter": "\\E082", + "fontCharacter": "\\E084", "fontColor": "#cc3e44" }, "_shell_light": { - "fontCharacter": "\\E085", + "fontCharacter": "\\E087", "fontColor": "#455155" }, "_shell": { - "fontCharacter": "\\E085", + "fontCharacter": "\\E087", "fontColor": "#4d5a5e" }, "_slim_light": { - "fontCharacter": "\\E086", + "fontCharacter": "\\E088", "fontColor": "#cc6d2e" }, "_slim": { - "fontCharacter": "\\E086", + "fontCharacter": "\\E088", "fontColor": "#e37933" }, "_smarty_light": { - "fontCharacter": "\\E087", + "fontCharacter": "\\E089", "fontColor": "#b7b73b" }, "_smarty": { - "fontCharacter": "\\E087", + "fontCharacter": "\\E089", "fontColor": "#cbcb41" }, "_spring_light": { - "fontCharacter": "\\E088", + "fontCharacter": "\\E08A", "fontColor": "#7fae42" }, "_spring": { - "fontCharacter": "\\E088", + "fontCharacter": "\\E08A", "fontColor": "#8dc149" }, "_stylelint_light": { - "fontCharacter": "\\E089", + "fontCharacter": "\\E08B", "fontColor": "#bfc2c1" }, "_stylelint": { - "fontCharacter": "\\E089", + "fontCharacter": "\\E08B", "fontColor": "#d4d7d6" }, "_stylelint_1_light": { - "fontCharacter": "\\E089", + "fontCharacter": "\\E08B", "fontColor": "#455155" }, "_stylelint_1": { - "fontCharacter": "\\E089", + "fontCharacter": "\\E08B", "fontColor": "#4d5a5e" }, "_stylus_light": { - "fontCharacter": "\\E08A", + "fontCharacter": "\\E08C", "fontColor": "#7fae42" }, "_stylus": { - "fontCharacter": "\\E08A", + "fontCharacter": "\\E08C", "fontColor": "#8dc149" }, "_sublime_light": { - "fontCharacter": "\\E08B", + "fontCharacter": "\\E08D", "fontColor": "#cc6d2e" }, "_sublime": { - "fontCharacter": "\\E08B", + "fontCharacter": "\\E08D", "fontColor": "#e37933" }, "_svelte_light": { - "fontCharacter": "\\E08C", + "fontCharacter": "\\E08E", "fontColor": "#b8383d" }, "_svelte": { - "fontCharacter": "\\E08C", + "fontCharacter": "\\E08E", "fontColor": "#cc3e44" }, "_svg_light": { - "fontCharacter": "\\E08D", + "fontCharacter": "\\E08F", "fontColor": "#9068b0" }, "_svg": { - "fontCharacter": "\\E08D", + "fontCharacter": "\\E08F", "fontColor": "#a074c4" }, "_svg_1_light": { - "fontCharacter": "\\E08D", + "fontCharacter": "\\E08F", "fontColor": "#498ba7" }, "_svg_1": { - "fontCharacter": "\\E08D", + "fontCharacter": "\\E08F", "fontColor": "#519aba" }, "_swift_light": { - "fontCharacter": "\\E08E", + "fontCharacter": "\\E090", "fontColor": "#cc6d2e" }, "_swift": { - "fontCharacter": "\\E08E", + "fontCharacter": "\\E090", "fontColor": "#e37933" }, "_terraform_light": { - "fontCharacter": "\\E08F", + "fontCharacter": "\\E091", "fontColor": "#9068b0" }, "_terraform": { - "fontCharacter": "\\E08F", + "fontCharacter": "\\E091", "fontColor": "#a074c4" }, "_tex_light": { - "fontCharacter": "\\E090", + "fontCharacter": "\\E092", "fontColor": "#498ba7" }, "_tex": { - "fontCharacter": "\\E090", + "fontCharacter": "\\E092", "fontColor": "#519aba" }, "_tex_1_light": { - "fontCharacter": "\\E090", + "fontCharacter": "\\E092", "fontColor": "#b7b73b" }, "_tex_1": { - "fontCharacter": "\\E090", + "fontCharacter": "\\E092", "fontColor": "#cbcb41" }, "_tex_2_light": { - "fontCharacter": "\\E090", + "fontCharacter": "\\E092", "fontColor": "#cc6d2e" }, "_tex_2": { - "fontCharacter": "\\E090", + "fontCharacter": "\\E092", "fontColor": "#e37933" }, "_tex_3_light": { - "fontCharacter": "\\E090", + "fontCharacter": "\\E092", "fontColor": "#bfc2c1" }, "_tex_3": { - "fontCharacter": "\\E090", + "fontCharacter": "\\E092", "fontColor": "#d4d7d6" }, "_todo": { - "fontCharacter": "\\E092" + "fontCharacter": "\\E094" }, "_tsconfig_light": { - "fontCharacter": "\\E093", + "fontCharacter": "\\E095", "fontColor": "#498ba7" }, "_tsconfig": { - "fontCharacter": "\\E093", + "fontCharacter": "\\E095", "fontColor": "#519aba" }, "_twig_light": { - "fontCharacter": "\\E094", + "fontCharacter": "\\E096", "fontColor": "#7fae42" }, "_twig": { - "fontCharacter": "\\E094", + "fontCharacter": "\\E096", "fontColor": "#8dc149" }, "_typescript_light": { - "fontCharacter": "\\E095", + "fontCharacter": "\\E097", "fontColor": "#498ba7" }, "_typescript": { - "fontCharacter": "\\E095", + "fontCharacter": "\\E097", "fontColor": "#519aba" }, "_typescript_1_light": { - "fontCharacter": "\\E095", + "fontCharacter": "\\E097", "fontColor": "#b7b73b" }, "_typescript_1": { - "fontCharacter": "\\E095", + "fontCharacter": "\\E097", "fontColor": "#cbcb41" }, "_vala_light": { - "fontCharacter": "\\E096", + "fontCharacter": "\\E098", "fontColor": "#627379" }, "_vala": { - "fontCharacter": "\\E096", + "fontCharacter": "\\E098", "fontColor": "#6d8086" }, "_video_light": { - "fontCharacter": "\\E097", + "fontCharacter": "\\E099", "fontColor": "#dd4b78" }, "_video": { - "fontCharacter": "\\E097", + "fontCharacter": "\\E099", "fontColor": "#f55385" }, "_vue_light": { - "fontCharacter": "\\E098", + "fontCharacter": "\\E09A", "fontColor": "#7fae42" }, "_vue": { - "fontCharacter": "\\E098", + "fontCharacter": "\\E09A", "fontColor": "#8dc149" }, "_wasm_light": { - "fontCharacter": "\\E099", + "fontCharacter": "\\E09B", "fontColor": "#9068b0" }, "_wasm": { - "fontCharacter": "\\E099", + "fontCharacter": "\\E09B", "fontColor": "#a074c4" }, "_wat_light": { - "fontCharacter": "\\E09A", + "fontCharacter": "\\E09C", "fontColor": "#9068b0" }, "_wat": { - "fontCharacter": "\\E09A", + "fontCharacter": "\\E09C", "fontColor": "#a074c4" }, "_webpack_light": { - "fontCharacter": "\\E09B", + "fontCharacter": "\\E09D", "fontColor": "#498ba7" }, "_webpack": { - "fontCharacter": "\\E09B", + "fontCharacter": "\\E09D", "fontColor": "#519aba" }, "_wgt_light": { - "fontCharacter": "\\E09C", + "fontCharacter": "\\E09E", "fontColor": "#498ba7" }, "_wgt": { - "fontCharacter": "\\E09C", + "fontCharacter": "\\E09E", "fontColor": "#519aba" }, "_windows_light": { - "fontCharacter": "\\E09D", + "fontCharacter": "\\E09F", "fontColor": "#498ba7" }, "_windows": { - "fontCharacter": "\\E09D", + "fontCharacter": "\\E09F", "fontColor": "#519aba" }, "_word_light": { - "fontCharacter": "\\E09E", + "fontCharacter": "\\E0A0", "fontColor": "#498ba7" }, "_word": { - "fontCharacter": "\\E09E", + "fontCharacter": "\\E0A0", "fontColor": "#519aba" }, "_xls_light": { - "fontCharacter": "\\E09F", + "fontCharacter": "\\E0A1", "fontColor": "#7fae42" }, "_xls": { - "fontCharacter": "\\E09F", + "fontCharacter": "\\E0A1", "fontColor": "#8dc149" }, "_xml_light": { - "fontCharacter": "\\E0A0", + "fontCharacter": "\\E0A2", "fontColor": "#cc6d2e" }, "_xml": { - "fontCharacter": "\\E0A0", + "fontCharacter": "\\E0A2", "fontColor": "#e37933" }, "_yarn_light": { - "fontCharacter": "\\E0A1", + "fontCharacter": "\\E0A3", "fontColor": "#498ba7" }, "_yarn": { - "fontCharacter": "\\E0A1", + "fontCharacter": "\\E0A3", "fontColor": "#519aba" }, "_yml_light": { - "fontCharacter": "\\E0A2", + "fontCharacter": "\\E0A4", "fontColor": "#9068b0" }, "_yml": { - "fontCharacter": "\\E0A2", + "fontCharacter": "\\E0A4", "fontColor": "#a074c4" }, "_zip_light": { - "fontCharacter": "\\E0A3", + "fontCharacter": "\\E0A5", "fontColor": "#b8383d" }, "_zip": { - "fontCharacter": "\\E0A3", + "fontCharacter": "\\E0A5", "fontColor": "#cc3e44" }, "_zip_1_light": { - "fontCharacter": "\\E0A3", + "fontCharacter": "\\E0A5", "fontColor": "#627379" }, "_zip_1": { - "fontCharacter": "\\E0A3", + "fontCharacter": "\\E0A5", "fontColor": "#6d8086" }, // {{SQL CARBON EDIT}} @@ -1576,7 +1600,6 @@ "jinja2": "_jinja", "kt": "_kotlin", "kts": "_kotlin", - "dart": "_dart", "liquid": "_liquid", "ls": "_livescript", "argdown": "_argdown", @@ -1609,12 +1632,15 @@ "prisma": "_prisma", "pp": "_puppet", "epp": "_puppet", + "purs": "_purescript", "spec.jsx": "_react_1", "test.jsx": "_react_1", "cjsx": "_react", "spec.tsx": "_react_2", "test.tsx": "_react_2", "re": "_reasonml", + "res": "_rescript", + "resi": "_rescript_1", "r": "_R", "rmd": "_R", "erb": "_html_erb", @@ -1768,6 +1794,8 @@ "webpack.prod.js": "_webpack", "license": "_license", "licence": "_license", + "license.md": "_license", + "licence.md": "_license", "copying": "_license", "compiling": "_license_1", "contributing": "_license_2", @@ -1777,6 +1805,7 @@ "procfile": "_heroku", "todo": "_todo", "npm-debug.log": "_npm_ignored", + // {{SQL CARBON EDIT}} "dashboard": "_shell", "profiler": "_csv", "Schema Compare": "scmp_dark", @@ -1791,6 +1820,7 @@ "cuda-cpp": "_cu", "csharp": "_c-sharp", "css": "_css", + "dart": "_dart", "dockerfile": "_docker", "ignore": "_git", "fsharp": "_f-sharp", @@ -1844,6 +1874,7 @@ "mustache": "_mustache", "nunjucks": "_nunjucks", "ocaml": "_ocaml", + "rescript": "_rescript", "sass": "_sass", "stylus": "_stylus", "terraform": "_terraform", @@ -1938,7 +1969,6 @@ "jinja2": "_jinja_light", "kt": "_kotlin_light", "kts": "_kotlin_light", - "dart": "_dart_light", "liquid": "_liquid_light", "ls": "_livescript_light", "argdown": "_argdown_light", @@ -1971,12 +2001,15 @@ "prisma": "_prisma_light", "pp": "_puppet_light", "epp": "_puppet_light", + "purs": "_purescript_light", "spec.jsx": "_react_1_light", "test.jsx": "_react_1_light", "cjsx": "_react_light", "spec.tsx": "_react_2_light", "test.tsx": "_react_2_light", "re": "_reasonml_light", + "res": "_rescript_light", + "resi": "_rescript_1_light", "r": "_R_light", "rmd": "_R_light", "erb": "_html_erb_light", @@ -2091,6 +2124,7 @@ "cuda-cpp": "_cu_light", "csharp": "_c-sharp_light", "css": "_css_light", + "dart": "_dart_light", "dockerfile": "_docker_light", "ignore": "_git_light", "fsharp": "_f-sharp_light", @@ -2144,6 +2178,7 @@ "mustache": "_mustache_light", "nunjucks": "_nunjucks_light", "ocaml": "_ocaml_light", + "rescript": "_rescript_light", "sass": "_sass_light", "stylus": "_stylus_light", "terraform": "_terraform_light", @@ -2204,6 +2239,8 @@ "webpack.prod.js": "_webpack_light", "license": "_license_light", "licence": "_license_light", + "license.md": "_license_light", + "licence.md": "_license_light", "copying": "_license_light", "compiling": "_license_1_light", "contributing": "_license_2_light", @@ -2212,10 +2249,11 @@ "cmakelists.txt": "_makefile_3_light", "procfile": "_heroku_light", "npm-debug.log": "_npm_ignored_light", + // {{SQL CARBON EDIT}} "dashboard": "_shell_light", "profiler": "_csv_light", "Schema Compare": "scmp" } }, - "version": "https://github.com/jesseweed/seti-ui/commit/894c49fa9f5ce43dd4f012173a7bb107346e524e" + "version": "https://github.com/jesseweed/seti-ui/commit/e80b920f4268e5daf1f65183dae33297aa4d0d17" } diff --git a/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json b/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json index 24e54ca9d3..82a0112f60 100644 --- a/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json +++ b/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json @@ -462,7 +462,7 @@ "statusBar.debuggingBackground": "#00212B", "statusBar.noFolderBackground": "#00212B", "statusBarItem.remoteBackground": "#2AA19899", - "ports.iconRunningProcessforeground": "#369432", + "ports.iconRunningProcessForeground": "#369432", "statusBarItem.prominentBackground": "#003847", "statusBarItem.prominentHoverBackground": "#003847", // "statusBarItem.activeBackground": "", diff --git a/extensions/theme-solarized-light/themes/solarized-light-color-theme.json b/extensions/theme-solarized-light/themes/solarized-light-color-theme.json index 3265ceb664..dfa65c9411 100644 --- a/extensions/theme-solarized-light/themes/solarized-light-color-theme.json +++ b/extensions/theme-solarized-light/themes/solarized-light-color-theme.json @@ -7,7 +7,10 @@ } }, { - "scope": ["meta.embedded", "source.groovy.embedded"], + "scope": [ + "meta.embedded", + "source.groovy.embedded" + ], "settings": { "foreground": "#657B83" } @@ -315,15 +318,12 @@ } ], "colors": { - // Base // "foreground": "", "focusBorder": "#D3AF86", // "contrastActiveBorder": "", // "contrastBorder": "", - // "widget.shadow": "", - "input.background": "#DDD6C1", // "input.border": "", "input.foreground": "#586E75", @@ -335,31 +335,24 @@ // "inputValidation.warningBorder": "", // "inputValidation.errorBackground": "", // "inputValidation.errorBorder": "", - "badge.background": "#B58900AA", "progressBar.background": "#B58900", - "dropdown.background": "#EEE8D5", // "dropdown.foreground": "", "dropdown.border": "#D3AF86", - "button.background": "#AC9D57", // "button.foreground": "", - - "selection.background": "#CCC4B0", - + "selection.background": "#878b9180", "list.activeSelectionBackground": "#DFCA88", "list.activeSelectionForeground": "#6C6C6C", "quickInputList.focusBackground": "#DFCA8866", "list.hoverBackground": "#DFCA8844", "list.inactiveSelectionBackground": "#D1CBB8", "list.highlightForeground": "#B58900", - // "scrollbar.shadow": "", // "scrollbarSlider.activeBackground": "", // "scrollbarSlider.background": "", // "scrollbarSlider.hoverBackground": "", - // Editor "editor.background": "#FDF6E3", // "editor.foreground": "#6688cc", @@ -389,14 +382,12 @@ // "editor.selectionHighlightBackground": "", // "editor.wordHighlightBackground": "", // "editor.wordHighlightStrongBackground": "", - // Editor: Suggest Widget // "editorSuggestWidget.background": "", // "editorSuggestWidget.border": "", // "editorSuggestWidget.foreground": "", // "editorSuggestWidget.highlightForeground": "", // "editorSuggestWidget.selectedBackground": "", - // Editor: Peek View "peekViewResult.background": "#EEE8D5", // "peekViewResult.lineForeground": "", @@ -410,25 +401,21 @@ // "peekViewResult.matchHighlightBackground": "", // "peekViewTitleLabel.foreground": "", // "peekViewTitleDescription.foreground": "", - // Editor: Diff // "diffEditor.insertedTextBackground": "", // "diffEditor.insertedTextBorder": "", // "diffEditor.removedTextBackground": "", // "diffEditor.removedTextBorder": "", - // Workbench: Title "titleBar.activeBackground": "#EEE8D5", // "titleBar.activeForeground": "", // "titleBar.inactiveBackground": "", // "titleBar.inactiveForeground": "", - // Workbench: Editors // "editorGroupHeader.noTabsBackground": "", "editorGroup.border": "#DDD6C1", "editorGroup.dropBackground": "#DDD6C1AA", "editorGroupHeader.tabsBackground": "#D9D2C2", - // Workbench: Tabs "tab.border": "#DDD6C1", "tab.activeBackground": "#FDF6E3", @@ -439,25 +426,21 @@ // "tab.activeForeground": "", // "tab.inactiveForeground": "", "tab.lastPinnedBorder": "#FDF6E3", - // Workbench: Activity Bar "activityBar.background": "#DDD6C1", "activityBar.foreground": "#584c27", "activityBarBadge.background": "#B58900", // "activityBarBadge.foreground": "", - // Workbench: Panel // "panel.background": "", "panel.border": "#DDD6C1", // "panelTitle.activeBorder": "", // "panelTitle.activeForeground": "", // "panelTitle.inactiveForeground": "", - // Workbench: Side Bar "sideBar.background": "#EEE8D5", "sideBarTitle.foreground": "#586E75", // "sideBarSectionHeader.background": "", - // Workbench: Status Bar "statusBar.foreground": "#586E75", "statusBar.background": "#EEE8D5", @@ -465,25 +448,21 @@ "statusBar.noFolderBackground": "#EEE8D5", // "statusBar.foreground": "", "statusBarItem.remoteBackground": "#AC9D57", - "ports.iconRunningProcessforeground": "#2AA19899", + "ports.iconRunningProcessForeground": "#2AA19899", "statusBarItem.prominentBackground": "#DDD6C1", "statusBarItem.prominentHoverBackground": "#DDD6C199", // "statusBarItem.activeBackground": "", // "statusBarItem.hoverBackground": "", - // Workbench: Debug "debugToolBar.background": "#DDD6C1", "debugExceptionWidget.background": "#DDD6C1", "debugExceptionWidget.border": "#AB395B", - // Workbench: Quick Open "pickerGroup.border": "#2AA19899", "pickerGroup.foreground": "#2AA19899", - // Extensions "extensionButton.prominentBackground": "#b58900", "extensionButton.prominentHoverBackground": "#584c27aa", - // Workbench: Terminal // Colors sourced from the official palette http://ethanschoonover.com/solarized "terminal.ansiBlack": "#073642", @@ -502,7 +481,6 @@ "terminal.ansiBrightMagenta": "#6c71c4", "terminal.ansiBrightCyan": "#93a1a1", "terminal.ansiBrightWhite": "#eee8d5", - // Interactive Playground "walkThrough.embeddedEditorBackground": "#00000014" }, diff --git a/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-color-theme.json b/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-color-theme.json index 8c2772bccf..efbe3a6a23 100644 --- a/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-color-theme.json +++ b/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-color-theme.json @@ -32,7 +32,7 @@ "titleBar.activeBackground": "#001126", "statusBar.background": "#001126", "statusBarItem.remoteBackground": "#0e639c", - "ports.iconRunningProcessforeground": "#bbdaff", + "ports.iconRunningProcessForeground": "#bbdaff", "statusBar.noFolderBackground": "#001126", "statusBar.debuggingBackground": "#001126", "activityBar.background": "#001733", diff --git a/extensions/tsconfig.base.json b/extensions/tsconfig.base.json index a884c12252..88563a0d5a 100644 --- a/extensions/tsconfig.base.json +++ b/extensions/tsconfig.base.json @@ -1,11 +1,13 @@ { "compilerOptions": { - "target": "es2018", + "target": "es2019", "lib": [ "es2019" ], "module": "commonjs", "strict": true, + "strictOptionalProperties": false, + "useUnknownInCatchVariables": false, "alwaysStrict": true, "noImplicitAny": true, "noImplicitReturns": true, diff --git a/extensions/vscode-colorize-tests/test/colorize-fixtures/test.dart b/extensions/vscode-colorize-tests/test/colorize-fixtures/test.dart new file mode 100644 index 0000000000..e26254197b --- /dev/null +++ b/extensions/vscode-colorize-tests/test/colorize-fixtures/test.dart @@ -0,0 +1,19 @@ +// from https://flutter.dev/ + +import 'package:flutter/material.dart'; + +void main() async { + runApp( + MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + body: MyApp(), + ), + ), + ); +} + +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} \ No newline at end of file diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_bat.json b/extensions/vscode-colorize-tests/test/colorize-results/test_bat.json index eb76b0e1d0..399fd0c220 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_bat.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_bat.json @@ -100,13 +100,13 @@ }, { "c": "%", - "t": "source.batchfile punctuation.definition.variable.batchfile", + "t": "source.batchfile variable.parameter.batchfile punctuation.definition.variable.batchfile", "r": { - "dark_plus": "default: #D4D4D4", - "light_plus": "default: #000000", + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "default: #FFFFFF" + "hc_black": "variable: #9CDCFE" } }, { diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_cu.json b/extensions/vscode-colorize-tests/test/colorize-results/test_cu.json index 21c8a0613c..6a652e7a3e 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_cu.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_cu.json @@ -806,8 +806,8 @@ "c": "%s", "t": "source.cuda-cpp meta.preprocessor.macro.cuda-cpp meta.block.cpp meta.block.cpp string.quoted.double.cpp constant.other.placeholder", "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", + "dark_plus": "constant.other.placeholder: #9CDCFE", + "light_plus": "constant.other.placeholder: #001080", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", "hc_black": "string: #CE9178" @@ -839,8 +839,8 @@ "c": "%s", "t": "source.cuda-cpp meta.preprocessor.macro.cuda-cpp meta.block.cpp meta.block.cpp string.quoted.double.cpp constant.other.placeholder", "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", + "dark_plus": "constant.other.placeholder: #9CDCFE", + "light_plus": "constant.other.placeholder: #001080", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", "hc_black": "string: #CE9178" @@ -861,8 +861,8 @@ "c": "%d", "t": "source.cuda-cpp meta.preprocessor.macro.cuda-cpp meta.block.cpp meta.block.cpp string.quoted.double.cpp constant.other.placeholder", "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", + "dark_plus": "constant.other.placeholder: #9CDCFE", + "light_plus": "constant.other.placeholder: #001080", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", "hc_black": "string: #CE9178" @@ -1752,8 +1752,8 @@ "c": "%s", "t": "source.cuda-cpp meta.preprocessor.macro.cuda-cpp meta.block.cpp meta.block.cpp string.quoted.double.cpp constant.other.placeholder", "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", + "dark_plus": "constant.other.placeholder: #9CDCFE", + "light_plus": "constant.other.placeholder: #001080", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", "hc_black": "string: #CE9178" @@ -1785,8 +1785,8 @@ "c": "%x", "t": "source.cuda-cpp meta.preprocessor.macro.cuda-cpp meta.block.cpp meta.block.cpp string.quoted.double.cpp constant.other.placeholder", "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", + "dark_plus": "constant.other.placeholder: #9CDCFE", + "light_plus": "constant.other.placeholder: #001080", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", "hc_black": "string: #CE9178" @@ -1807,8 +1807,8 @@ "c": "%s", "t": "source.cuda-cpp meta.preprocessor.macro.cuda-cpp meta.block.cpp meta.block.cpp string.quoted.double.cpp constant.other.placeholder", "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", + "dark_plus": "constant.other.placeholder: #9CDCFE", + "light_plus": "constant.other.placeholder: #001080", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", "hc_black": "string: #CE9178" @@ -1829,8 +1829,8 @@ "c": "%d", "t": "source.cuda-cpp meta.preprocessor.macro.cuda-cpp meta.block.cpp meta.block.cpp string.quoted.double.cpp constant.other.placeholder", "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", + "dark_plus": "constant.other.placeholder: #9CDCFE", + "light_plus": "constant.other.placeholder: #001080", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", "hc_black": "string: #CE9178" @@ -11344,8 +11344,8 @@ "c": "%u", "t": "source.cuda-cpp meta.function.definition.cuda-cpp meta.body.function.definition.cuda-cpp meta.block.cuda-cpp meta.block.cuda-cpp string.quoted.double.cuda-cpp constant.other.placeholder", "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", + "dark_plus": "constant.other.placeholder: #9CDCFE", + "light_plus": "constant.other.placeholder: #001080", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", "hc_black": "string: #CE9178" @@ -11366,8 +11366,8 @@ "c": "%u", "t": "source.cuda-cpp meta.function.definition.cuda-cpp meta.body.function.definition.cuda-cpp meta.block.cuda-cpp meta.block.cuda-cpp string.quoted.double.cuda-cpp constant.other.placeholder", "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", + "dark_plus": "constant.other.placeholder: #9CDCFE", + "light_plus": "constant.other.placeholder: #001080", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", "hc_black": "string: #CE9178" @@ -11388,8 +11388,8 @@ "c": "%u", "t": "source.cuda-cpp meta.function.definition.cuda-cpp meta.body.function.definition.cuda-cpp meta.block.cuda-cpp meta.block.cuda-cpp string.quoted.double.cuda-cpp constant.other.placeholder", "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", + "dark_plus": "constant.other.placeholder: #9CDCFE", + "light_plus": "constant.other.placeholder: #001080", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", "hc_black": "string: #CE9178" diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_dart.json b/extensions/vscode-colorize-tests/test/colorize-results/test_dart.json new file mode 100644 index 0000000000..0d944b2b5a --- /dev/null +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_dart.json @@ -0,0 +1,673 @@ +[ + { + "c": "// from https://flutter.dev/", + "t": "source.dart comment.line.double-slash.dart", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668" + } + }, + { + "c": "import", + "t": "source.dart meta.declaration.dart keyword.other.import.dart", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6" + } + }, + { + "c": " ", + "t": "source.dart meta.declaration.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "'package:flutter/material.dart'", + "t": "source.dart meta.declaration.dart string.interpolated.single.dart", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178" + } + }, + { + "c": ";", + "t": "source.dart meta.declaration.dart punctuation.terminator.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "void", + "t": "source.dart storage.type.primitive.dart", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "main", + "t": "source.dart entity.name.function.dart", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA" + } + }, + { + "c": "() ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "async", + "t": "source.dart keyword.control.dart", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0" + } + }, + { + "c": " {", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "runApp", + "t": "source.dart entity.name.function.dart", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA" + } + }, + { + "c": "(", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "MaterialApp", + "t": "source.dart support.class.dart", + "r": { + "dark_plus": "support.class: #4EC9B0", + "light_plus": "support.class: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.class: #4EC9B0" + } + }, + { + "c": "(", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " debugShowCheckedModeBanner", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ":", + "t": "source.dart keyword.operator.ternary.dart", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "false", + "t": "source.dart constant.language.dart", + "r": { + "dark_plus": "constant.language: #569CD6", + "light_plus": "constant.language: #0000FF", + "dark_vs": "constant.language: #569CD6", + "light_vs": "constant.language: #0000FF", + "hc_black": "constant.language: #569CD6" + } + }, + { + "c": ",", + "t": "source.dart punctuation.comma.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " home", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ":", + "t": "source.dart keyword.operator.ternary.dart", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "Scaffold", + "t": "source.dart support.class.dart", + "r": { + "dark_plus": "support.class: #4EC9B0", + "light_plus": "support.class: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.class: #4EC9B0" + } + }, + { + "c": "(", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " body", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ":", + "t": "source.dart keyword.operator.ternary.dart", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "MyApp", + "t": "source.dart support.class.dart", + "r": { + "dark_plus": "support.class: #4EC9B0", + "light_plus": "support.class: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.class: #4EC9B0" + } + }, + { + "c": "()", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ",", + "t": "source.dart punctuation.comma.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " )", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ",", + "t": "source.dart punctuation.comma.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " )", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ",", + "t": "source.dart punctuation.comma.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " )", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ";", + "t": "source.dart punctuation.terminator.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "}", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "class", + "t": "source.dart keyword.declaration.dart", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "MyApp", + "t": "source.dart support.class.dart", + "r": { + "dark_plus": "support.class: #4EC9B0", + "light_plus": "support.class: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.class: #4EC9B0" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "extends", + "t": "source.dart keyword.declaration.dart", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "StatefulWidget", + "t": "source.dart support.class.dart", + "r": { + "dark_plus": "support.class: #4EC9B0", + "light_plus": "support.class: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.class: #4EC9B0" + } + }, + { + "c": " {", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "@override", + "t": "source.dart storage.type.annotation.dart", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "_MyAppState", + "t": "source.dart support.class.dart", + "r": { + "dark_plus": "support.class: #4EC9B0", + "light_plus": "support.class: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.class: #4EC9B0" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "createState", + "t": "source.dart entity.name.function.dart", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA" + } + }, + { + "c": "() ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "=>", + "t": "source.dart keyword.operator.closure.dart", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "_MyAppState", + "t": "source.dart support.class.dart", + "r": { + "dark_plus": "support.class: #4EC9B0", + "light_plus": "support.class: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.class: #4EC9B0" + } + }, + { + "c": "()", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ";", + "t": "source.dart punctuation.terminator.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "}", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + } +] \ No newline at end of file diff --git a/extensions/vscode-notebook-tests/media/icon.png b/extensions/vscode-notebook-tests/media/icon.png deleted file mode 100644 index f785ef7031624d04d821a12d5f1534ecccfef491..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2210 zcmY*bc{tSD8$aI}!x-BnOVKh?jjlCH3uR;sS}es?-O7+kNe0PS=WFSfa0#VCqq?E8 zrYwaSx|S@zQ9@eGSh8fYMPrzm?;Y-6zw^A$`F!5xInU>P-t(O2#5+6MDdDtm001Qi zd-7obASs0a7A*zef}^fdc;S4|)mDlC=76vk5Zbo1hqZ#RHW1Mc!lmxAMJz}DdxhBCV00mfSD~OY7m5z6hd3z%kkzuSRqHWL>=NFrX zeXiWg2oCDIhM&@n5V1A>r3@QE*~O`wpQYHZ*J}Q%w#BCqzIm$NkZlms5rs^i=?Kf zQznejSKBf)$LVPn3bl4wdFYOe_zsC8lCqe^NhwSOMbx{p-9Pie@ooN3QC;3%5@ZZ9 zdk%g7(OqqH8XQ9)@3n6*v6X=;fazL~`oO|+RyE%hVcOiW3M7FRcGbZR3}~uwSY91{ z?}ak(#70$w+#mNFw+CyRVI@V(^k>&&FNP|&dhYJ5T9j~JR&We!(;B1AI_5>~#X*Ub z!-`03xI@Wq==#-n@+7iF4@t2wDU)Q`TbB`fJ-lttGkrp7Nq}Y|@%Tw7zHwB;p8O0A zPf_GRz{s#Hdx3uBGv5BW3hX++tY$49M;!0j%@=xC zHuj3t{hJnj72u>Vs`RLL;<~>CJsO1l2GK)KCuSrqC~smguKGK@Jl`CYAI@V-rZdFb zWv{m>;tfh{gQ59Hq=W%XdA_Z|S3;IwAhM!grrbHyq3n28J|EdN zrlWNxXA@iRz;2A%!1=Waq}Zz;gEQQ+WIlOGvcID)@0u#eV*NsK4irk&Vz#ZndSgM~ zm^aiznkjzB-p60_(L+EyHM&g|j~3!M||zCrSL>*Ba#2~4SH#(Dgd>_)GFn?e)UDhTzJ zc{j`X~7EJ;}49U_HW-!ej40r_ZQVWFRDtMiTm!U@HsPW^_9YoSTA!?|Y zxls++HF6JvHTpmEfL_DVTUL->Hca;)No4u2!owG)~xc#Zz;a$j%uhXCE z9a037`EV&1DrS*lj(+fnjK-E0NE=_#r}4`jp-tg89kr^NaEc(+DB|Oy^lCiCcCH73 z@tn={p(M#@|G|rm{&Yr7$W?9nriuYSI-^0wE}M6QH7K6jyY8An<=_uGBO96#F`Fa! zk{0RcOxdc`35U_IwoO|fE&%3ea6Ae_kv7`#$D1u?`MCW(h8H_3Q9q&(`-xP#pu!^#JAeIA9eZ+^0vgL7p41q?8?Z1z{nYW zr)S{-GrziyB~y06T$)e1d=&9+WXP+j4JDK=%RHc@(J%6HZ%_riz|~06k&|p_g@W*z z5k49&PiGCI)?sN4B6ZC>JY#Yw!z+(2i?{CcSnTZAAkmaB8igVWL4{_dOUf5tO*}V6 zu#CLyF(*tdpUbrGRERkD=G$F~D=csicBiKbt7U{opgs>bcm&1GATS03IE(rUG|= z$_$zL3%4mmP`dQaP=itU1CLh1cT6YLLs7E2O%F6yC7xKRIQ644F8s|}Ic{Bgk`ed^ zNj!m`meD1G`?r!_Q!R~Fp9Jw+dMoAsDQh-22lsO&I|i|_nGfWkn43x|#G`Y>7&pd- z|AT=K%@R(~?NZf1w1GxZ;)DS((5y7G-ueaCRTk)+O7#|wOs`f?+oN`Fzj>}zvIY6E zqVlCbI}>ql4e=Uzbz=ETsniQ|JyzHWW8SblMIu^|v(Dn#^`|8@FwcegdQXnr-P^CS z(Fiu#sp5r>tCI<_9IYdEE~K;n-rN7^W)-Kix&e`0u{@1z&a+B2cv_r2K8gXO!erx!e z?Z=K9nngudTWX2w>-q*Xl2qQfByQQX5R|7j_zo}XZO=qxGT}IST0OaF>9gTr>qsuz HM~(Rx0c1bk diff --git a/extensions/vscode-test-resolver/package.json b/extensions/vscode-test-resolver/package.json index 2256c04735..9aa252ea51 100644 --- a/extensions/vscode-test-resolver/package.json +++ b/extensions/vscode-test-resolver/package.json @@ -27,7 +27,13 @@ ], "main": "./out/extension", "devDependencies": { - "@types/node": "^12.19.9" + "@types/node": "14.x" + }, + "capabilities": { + "untrustedWorkspaces": { + "supported": true + }, + "virtualWorkspaces": true }, "contributes": { "resourceLabelFormatters": [ @@ -83,7 +89,7 @@ "statusBar/remoteIndicator": [ { "command": "vscode-testresolver.newWindow", - "when": "!remoteName", + "when": "!remoteName && !virtualWorkspace", "group": "remote_90_test_1_local@2" }, { diff --git a/extensions/vscode-test-resolver/src/extension.ts b/extensions/vscode-test-resolver/src/extension.ts index f7d5714b06..ec6590604c 100644 --- a/extensions/vscode-test-resolver/src/extension.ts +++ b/extensions/vscode-test-resolver/src/extension.ts @@ -52,7 +52,7 @@ export function activate(context: vscode.ExtensionContext) { const match = lastProgressLine.match(/Extension host agent listening on (\d+)/); if (match) { isResolved = true; - res(new vscode.ResolvedAuthority('localhost', parseInt(match[1], 10))); // success! + res(new vscode.ResolvedAuthority('127.0.0.1', parseInt(match[1], 10))); // success! } lastProgressLine = ''; } else if (chr === CharCode.Backspace) { @@ -200,7 +200,7 @@ export function activate(context: vscode.ExtensionContext) { } }); }); - proxyServer.listen(0, () => { + proxyServer.listen(0, '127.0.0.1', () => { const port = (proxyServer.address()).port; outputChannel.appendLine(`Going through proxy at port ${port}`); const r: vscode.ResolverResult = new vscode.ResolvedAuthority('127.0.0.1', port); @@ -216,6 +216,9 @@ export function activate(context: vscode.ExtensionContext) { } const authorityResolverDisposable = vscode.workspace.registerRemoteAuthorityResolver('test', { + async getCanonicalURI(uri: vscode.Uri): Promise { + return vscode.Uri.file(uri.path); + }, resolve(_authority: string): Thenable { return vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, @@ -265,7 +268,7 @@ export function activate(context: vscode.ExtensionContext) { const port = Number.parseInt(result); vscode.workspace.openTunnel({ remoteAddress: { - host: 'localhost', + host: '127.0.0.1', port: port }, localAddressPort: port + 1 @@ -403,10 +406,10 @@ async function tunnelFactory(tunnelOptions: vscode.TunnelOptions, tunnelCreation if (localPort < 1024 && process.platform !== 'win32') { localPort = 0; } - proxyServer.listen(localPort, () => { + proxyServer.listen(localPort, '127.0.0.1', () => { 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 }); + const tunnel = newTunnel({ host: '127.0.0.1', port: localPort }); tunnel.onDidDispose(() => proxyServer.close()); res(tunnel); }); @@ -420,8 +423,8 @@ function runHTTPTestServer(port: number): vscode.Disposable { 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}`; + server.listen(port, '127.0.0.1'); + const message = `Opened HTTP server on http://127.0.0.1:${port}`; console.log(message); outputChannel.appendLine(message); return { diff --git a/extensions/vscode-test-resolver/yarn.lock b/extensions/vscode-test-resolver/yarn.lock index ab43d14d56..995b2c2f8b 100644 --- a/extensions/vscode-test-resolver/yarn.lock +++ b/extensions/vscode-test-resolver/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@types/node@^12.19.9": - version "12.20.14" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.14.tgz#9caf7eea0df08b406829889cc015256a6d81ab10" - integrity sha512-iFJOS5Q470FF+r4Ol2pSley7/wCNVqf+jgjhtxLLaJcDs+To2iCxlXIkJXrGLD9w9G/oJ9ibySu7z92DCwr7Pg== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 067a5ebba6..78b52f40df 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -24,10 +24,10 @@ fast-plist@0.1.2: resolved "https://registry.yarnpkg.com/fast-plist/-/fast-plist-0.1.2.tgz#a45aff345196006d406ca6cdcd05f69051ef35b8" integrity sha1-pFr/NFGWAG1AbKbNzQX2kFHvNbg= -typescript@4.2.4: - version "4.2.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961" - integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg== +typescript@4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805" + integrity sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw== vscode-grammar-updater@^1.0.3: version "1.0.3" diff --git a/package.json b/package.json index e66ad47bc2..bcc320429c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "azuredatastudio", "version": "1.33.0", - "distro": "56629278c8076969bc8514aa6a3f129268312d5b", + "distro": "b927d42f1e4c2fb06c3f48024e90b0358583a194", "author": { "name": "Microsoft Corporation" }, @@ -14,7 +14,7 @@ "preinstall": "node build/npm/preinstall.js", "postinstall": "node build/npm/postinstall.js", "compile": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js compile", - "watch": "npm-run-all -lp watch-client watch-extensions watch-extension-media", + "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", @@ -24,8 +24,7 @@ "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": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js watch-extensions", - "watch-extension-media": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js watch-extension-media", + "watch-extensions": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js watch-extensions watch-extension-media", "watch-extensionsd": "deemon yarn watch-extensions", "kill-watch-extensionsd": "deemon --kill yarn watch-extensions", "mocha": "mocha test/unit/node/all.js --delay", @@ -75,7 +74,7 @@ "applicationinsights": "1.0.8", "chart.js": "^2.9.4", "chokidar": "3.5.2", - "graceful-fs": "4.2.3", + "graceful-fs": "4.2.6", "gridstack": "^3.1.3", "html-query-plan": "git://github.com/kburtram/html-query-plan.git#2.6", "html-to-image": "^1.6.2", @@ -83,37 +82,37 @@ "https-proxy-agent": "^2.2.3", "iconv-lite-umd": "0.6.8", "jquery": "3.5.0", - "jschardet": "2.3.0", - "keytar": "7.2.0", + "jschardet": "3.0.0", "mark.js": "^8.11.1", + "keytar": "7.2.0", "minimist": "^1.2.5", "native-is-elevated": "0.4.3", "native-keymap": "2.2.1", "native-watchdog": "1.3.0", "ng2-charts": "^1.6.0", - "node-pty": "0.10.1", + "node-pty": "0.11.0-beta7", "nsfw": "2.1.2", "plotly.js-dist-min": "^1.53.0", "reflect-metadata": "^0.1.8", "rxjs": "5.4.0", "sanitize-html": "1.19.1", "semver-umd": "^5.5.7", + "spdlog": "^0.13.0", "slickgrid": "github:Microsoft/SlickGrid.ADS#2.3.34", - "spdlog": "^0.11.1", "sudo-prompt": "9.2.1", "tas-client-umd": "0.1.4", "turndown": "^7.0.0", "v8-inspect-profiler": "^0.0.20", - "vscode-oniguruma": "1.3.1", + "vscode-oniguruma": "1.5.1", "vscode-proxy-agent": "^0.11.0", "vscode-regexpp": "^3.1.0", "vscode-ripgrep": "^1.11.3", "vscode-sqlite3": "4.0.11", - "vscode-textmate": "5.2.0", - "xterm": "4.12.0-beta.26", + "vscode-textmate": "5.4.0", + "xterm": "4.13.0-beta.1", "xterm-addon-search": "0.9.0-beta.2", "xterm-addon-unicode11": "0.3.0-beta.5", - "xterm-addon-webgl": "0.11.0-beta.8", + "xterm-addon-webgl": "0.11.1", "yauzl": "^2.9.2", "yazl": "^2.4.3", "zone.js": "^0.8.4" @@ -133,10 +132,11 @@ "@types/keytar": "^4.4.0", "@types/minimist": "^1.2.1", "@types/mocha": "^8.2.0", - "@types/node": "^14.14.37", + "@types/node": "14.x", "@types/plotly.js": "^1.44.9", "@types/sanitize-html": "^1.18.2", - "@types/sinon": "^1.16.36", + "@types/sinon": "^10.0.2", + "@types/sinon-test": "^2.4.2", "@types/trusted-types": "^1.0.6", "@types/vscode-windows-registry": "^1.0.0", "@types/webpack": "^4.41.25", @@ -218,7 +218,8 @@ "rcedit": "^1.1.0", "request": "^2.85.0", "rimraf": "^2.2.8", - "sinon": "^1.17.2", + "sinon": "^11.1.1", + "sinon-test": "^3.1.0", "source-map": "0.6.1", "source-map-support": "^0.3.2", "style-loader": "^1.0.0", @@ -226,14 +227,14 @@ "ts-loader": "^6.2.1", "tsec": "0.1.4", "typemoq": "^0.3.2", - "typescript": "^4.3.0-dev.20210426", + "typescript": "^4.4.0-dev.20210607", "typescript-formatter": "7.1.0", "underscore": "^1.12.1", "vinyl": "^2.0.0", "vinyl-fs": "^3.0.0", "vscode-debugprotocol": "1.47.0", "vscode-nls-dev": "^3.3.1", - "vscode-telemetry-extractor": "^1.7.0", + "vscode-telemetry-extractor": "^1.8.0", "webpack": "^4.43.0", "webpack-cli": "^3.3.12", "webpack-stream": "^5.2.1", @@ -249,8 +250,8 @@ }, "optionalDependencies": { "vscode-windows-registry": "1.0.3", - "windows-foreground-love": "0.2.0", - "windows-mutex": "0.3.0", + "windows-foreground-love": "0.4.0", + "windows-mutex": "0.4.1", "windows-process-tree": "0.3.0" }, "resolutions": { diff --git a/product.json b/product.json index 63516635bd..05d7e3a258 100644 --- a/product.json +++ b/product.json @@ -71,10 +71,10 @@ "extensionAllowedProposedApi": [ "ms-vscode.vscode-js-profile-flame", "ms-vscode.vscode-js-profile-table", - "ms-vscode.github-browser", - "ms-vscode.github-richnav", "ms-vscode.remotehub", - "ms-vscode.remotehub-insiders" + "ms-vscode.remotehub-insiders", + "GitHub.remotehub", + "GitHub.remotehub-insiders" ], "extensionsGallery": { "version": "0.0.76", @@ -84,7 +84,7 @@ { "name": "Microsoft.sqlservernotebook", "version": "0.4.0", - "repo": "https://github.com/Microsoft/azuredatastudio" + "repo": "https://github.com/microsoft/azuredatastudio" } ], "webBuiltInExtensions": [] diff --git a/remote/package.json b/remote/package.json index a93cabc8fe..5ca91afeba 100644 --- a/remote/package.json +++ b/remote/package.json @@ -17,7 +17,7 @@ "chart.js": "^2.9.4", "chokidar": "3.5.2", "cookie": "^0.4.0", - "graceful-fs": "4.2.3", + "graceful-fs": "4.2.6", "gridstack": "^3.1.3", "html-query-plan": "git://github.com/kburtram/html-query-plan.git#2.6", "html-to-image": "^1.6.2", @@ -25,38 +25,38 @@ "https-proxy-agent": "^2.2.3", "iconv-lite-umd": "0.6.8", "jquery": "3.5.0", - "jschardet": "2.3.0", + "jschardet": "3.0.0", "mark.js": "^8.11.1", "minimist": "^1.2.5", "native-watchdog": "1.3.0", "ng2-charts": "^1.6.0", - "node-pty": "0.10.1", + "node-pty": "0.11.0-beta7", "nsfw": "2.1.2", "plotly.js-dist-min": "^1.53.0", "reflect-metadata": "^0.1.8", "rxjs": "5.4.0", "sanitize-html": "1.19.1", "semver-umd": "^5.5.7", + "spdlog": "^0.13.0", "slickgrid": "github:Microsoft/SlickGrid.ADS#2.3.34", - "spdlog": "^0.11.1", "turndown": "^7.0.0", "turndown-plugin-gfm": "^1.0.2", "tas-client-umd": "0.1.4", - "vscode-oniguruma": "1.3.1", + "vscode-oniguruma": "1.5.1", "vscode-proxy-agent": "^0.11.0", "vscode-regexpp": "^3.1.0", "vscode-ripgrep": "^1.11.3", - "vscode-textmate": "5.2.0", - "xterm": "4.12.0-beta.26", + "vscode-textmate": "5.4.0", + "xterm": "4.13.0-beta.1", "xterm-addon-search": "0.9.0-beta.2", "xterm-addon-unicode11": "0.3.0-beta.5", - "xterm-addon-webgl": "0.11.0-beta.8", + "xterm-addon-webgl": "0.11.1", "yauzl": "^2.9.2", "yazl": "^2.4.3", "zone.js": "^0.8.4" }, "optionalDependencies": { - "vscode-windows-registry": "1.0.2", - "windows-process-tree": "0.2.4" + "vscode-windows-registry": "1.0.3", + "windows-process-tree": "0.3.0" } } diff --git a/remote/web/package.json b/remote/web/package.json index 074a5d92d7..b7790e1584 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -14,15 +14,15 @@ "angular2-grid": "2.0.6", "ansi_up": "^3.0.0", "chart.js": "^2.9.4", - "gridstack": "^3.1.3", + "gridstack": "^3.1.3", "html-query-plan": "git://github.com/kburtram/html-query-plan.git#2.6", - "html-to-image": "^1.6.2", + "html-to-image": "^1.6.2", "iconv-lite-umd": "0.6.8", - "jquery": "3.5.0", - "jschardet": "2.3.0", + "jquery": "3.5.0", + "jschardet": "3.0.0", "mark.js": "^8.11.1", "ng2-charts": "^1.6.0", - "plotly.js-dist-min": "^1.53.0", + "plotly.js-dist-min": "^1.53.0", "reflect-metadata": "^0.1.8", "rxjs": "5.4.0", "sanitize-html": "1.19.1", @@ -31,11 +31,11 @@ "turndown": "^7.0.0", "turndown-plugin-gfm": "^1.0.2", "tas-client-umd": "0.1.4", - "vscode-oniguruma": "1.3.1", - "vscode-textmate": "5.2.0", - "xterm": "4.12.0-beta.26", + "vscode-oniguruma": "1.5.1", + "vscode-textmate": "5.4.0", + "xterm": "4.13.0-beta.1", "xterm-addon-search": "0.9.0-beta.2", "xterm-addon-unicode11": "0.3.0-beta.5", - "xterm-addon-webgl": "0.11.0-beta.8" + "xterm-addon-webgl": "0.11.1" } } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index a6b78bbcf0..641d267176 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -212,10 +212,10 @@ jquery@3.5.0: resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.0.tgz#9980b97d9e4194611c36530e7dc46a58d7340fc9" integrity sha512-Xb7SVYMvygPxbFMpTFQiHh1J7HClEaThguL15N/Gg37Lri/qKyhRGZYzHRyLH8Stq3Aow0LsHO2O2ci86fCrNQ== -jschardet@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-2.3.0.tgz#06e2636e16c8ada36feebbdc08aa34e6a9b3ff75" - integrity sha512-6I6xT7XN/7sBB7q8ObzKbmv5vN+blzLcboDE1BNEsEfmRXJValMxO6OIRT69ylPBRemS3rw6US+CMCar0OBc9g== +jschardet@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882" + integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ== lodash.clonedeep@^4.5.0: version "4.5.0" @@ -383,15 +383,15 @@ util-deprecate@^1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -vscode-oniguruma@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.3.1.tgz#e2383879c3485b19f533ec34efea9d7a2b14be8f" - integrity sha512-gz6ZBofA7UXafVA+m2Yt2zHKgXC2qedArprIsHAPKByTkwq9l5y/izAGckqxYml7mSbYxTRTfdRwsFq3cwF4LQ== +vscode-oniguruma@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.5.1.tgz#9ca10cd3ada128bd6380344ea28844243d11f695" + integrity sha512-JrBZH8DCC262TEYcYdeyZusiETu0Vli0xFgdRwNJjDcObcRjbmJP+IFcA3ScBwIXwgFHYKbAgfxtM/Cl+3Spjw== -vscode-textmate@5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e" - integrity sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ== +vscode-textmate@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.4.0.tgz#4b25ffc1f14ac3a90faf9a388c67a01d24257cd7" + integrity sha512-c0Q4zYZkcLizeYJ3hNyaVUM2AA8KDhNCA3JvXY8CeZSJuBdAy3bAvSbv46RClC4P3dSO9BdwhnKEx2zOo6vP/w== xtend@^4.0.0: version "4.0.2" @@ -408,12 +408,12 @@ xterm-addon-unicode11@0.3.0-beta.5: resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.3.0-beta.5.tgz#7e490799d530d3b301125c7a4e92317c161761c4" integrity sha512-SgDDL3PoMH1G48JO6T45whKAex4NPxi80UzUVitnrqyd8dFQP+oF6cxqUutULgm9HSGk62qy3mrZvIMGO5VXog== -xterm-addon-webgl@0.11.0-beta.8: - version "0.11.0-beta.8" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.11.0-beta.8.tgz#8cb4925d67c31beb8144275daf46358f42eff9fe" - integrity sha512-udRmQ/jgH8cL8VQOZweytkToIROevVeiA7WY0tIe878Wt2zKY+AYHZV8js3c1W9wHDu5G90BhmzTidJ5UwZK3Q== +xterm-addon-webgl@0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.11.1.tgz#33dd250ab52e9f51d2ff52396447962e6f53e24c" + integrity sha512-xF6DnEoV+rPtzetMBXBZVe1kLKtus7AKdEcyfq2eMHQzhaRvC+pfnU+XiCXC85kueguqu2UkBHXZs5mihK9jOQ== -xterm@4.12.0-beta.26: - version "4.12.0-beta.26" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.12.0-beta.26.tgz#57c75b732808795398a66bc1a3e06d09eaff2ada" - integrity sha512-yZB1kMBXQu2G0G1ch7TUi6f893iTZC+tmfjw/PQNZTmN46b4oX1l7rplc3sFcdrICHtmQ0Q5n1u0d6WUAdq1Kw== +xterm@4.13.0-beta.1: + version "4.13.0-beta.1" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.13.0-beta.1.tgz#ad2ad321c69a4add6e878c890f278a1da74fd7d0" + integrity sha512-gAMGqBglESTxQWph1uKVyd1jO/6eKsbicNG+Mr/YAsj06TjFVcLw839Iqu6P+DVFEV7lLLspcOb8fwX6qMBH/Q== diff --git a/remote/yarn.lock b/remote/yarn.lock index 07d044dc47..27c02d479f 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -378,12 +378,7 @@ glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -graceful-fs@4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" - integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== - -graceful-fs@^4.1.6, graceful-fs@^4.2.0: +graceful-fs@4.2.6, graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.6" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== @@ -509,10 +504,10 @@ jquery@3.5.0: resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.0.tgz#9980b97d9e4194611c36530e7dc46a58d7340fc9" integrity sha512-Xb7SVYMvygPxbFMpTFQiHh1J7HClEaThguL15N/Gg37Lri/qKyhRGZYzHRyLH8Stq3Aow0LsHO2O2ci86fCrNQ== -jschardet@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-2.3.0.tgz#06e2636e16c8ada36feebbdc08aa34e6a9b3ff75" - integrity sha512-6I6xT7XN/7sBB7q8ObzKbmv5vN+blzLcboDE1BNEsEfmRXJValMxO6OIRT69ylPBRemS3rw6US+CMCar0OBc9g== +jschardet@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882" + integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ== jsonfile@^4.0.0: version "4.0.0" @@ -556,7 +551,7 @@ minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -mkdirp@^0.5.1: +mkdirp@^0.5.5: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -605,10 +600,10 @@ node-addon-api@*, node-addon-api@^3.0.2: 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.1: - version "0.10.1" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.10.1.tgz#cd05d03a2710315ec40221232ec04186f6ac2c6d" - integrity sha512-JTdtUS0Im/yRsWJSx7yiW9rtpfmxqxolrtnyKwPLI+6XqTAPW/O2MjS8FYL4I5TsMbH2lVgDb2VMjp+9LoQGNg== +node-pty@0.11.0-beta7: + version "0.11.0-beta7" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.11.0-beta7.tgz#aed0888b5032d96c54d8473455e6adfae3bbebbe" + integrity sha512-uApPGLglZRiHQcUMWakbZOrBo8HVWvhzIqNnrWvBGJOvc6m/S5lCdbbg93BURyJqHFmBS0GV+4hwiMNDuGRbSA== dependencies: nan "^2.14.0" @@ -763,13 +758,13 @@ source-map@^0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -spdlog@^0.11.1: - version "0.11.1" - resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.11.1.tgz#29721b31018a5fe6a3ce2531f9d8d43e0bd6b825" - integrity sha512-M+sg9/Tnr0lrfnW2/hqgpoc4Z8Jzq7W8NUn35iiSslj+1uj1pgutI60MCpulDP2QyFzOpC8VsJmYD6Fub7wHoA== +spdlog@^0.13.0: + version "0.13.5" + resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.13.5.tgz#a31027dcccbe032e9a53579f42cb45428af08bad" + integrity sha512-D1xA5tRXw7eZOoFBCAnOxCxLN3JpHVDjpPJG/xjJ0nFZvtfOUTAzK66MVxJCDht/ZFwjLcBAltvzjfz4JTuSEw== dependencies: bindings "^1.5.0" - mkdirp "^0.5.1" + mkdirp "^0.5.5" nan "^2.14.0" srcset@^1.0.0: @@ -838,10 +833,10 @@ util-deprecate@^1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -vscode-oniguruma@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.3.1.tgz#e2383879c3485b19f533ec34efea9d7a2b14be8f" - integrity sha512-gz6ZBofA7UXafVA+m2Yt2zHKgXC2qedArprIsHAPKByTkwq9l5y/izAGckqxYml7mSbYxTRTfdRwsFq3cwF4LQ== +vscode-oniguruma@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.5.1.tgz#9ca10cd3ada128bd6380344ea28844243d11f695" + integrity sha512-JrBZH8DCC262TEYcYdeyZusiETu0Vli0xFgdRwNJjDcObcRjbmJP+IFcA3ScBwIXwgFHYKbAgfxtM/Cl+3Spjw== vscode-proxy-agent@^0.11.0: version "0.11.0" @@ -871,10 +866,10 @@ vscode-ripgrep@^1.11.3: https-proxy-agent "^4.0.0" proxy-from-env "^1.1.0" -vscode-textmate@5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e" - integrity sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ== +vscode-textmate@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.4.0.tgz#4b25ffc1f14ac3a90faf9a388c67a01d24257cd7" + integrity sha512-c0Q4zYZkcLizeYJ3hNyaVUM2AA8KDhNCA3JvXY8CeZSJuBdAy3bAvSbv46RClC4P3dSO9BdwhnKEx2zOo6vP/w== vscode-windows-ca-certs@^0.3.0: version "0.3.0" @@ -883,15 +878,15 @@ vscode-windows-ca-certs@^0.3.0: dependencies: node-addon-api "^3.0.2" -vscode-windows-registry@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/vscode-windows-registry/-/vscode-windows-registry-1.0.2.tgz#b863e704a6a69c50b3098a55fbddbe595b0c124a" - integrity sha512-/CLLvuOSM2Vme2z6aNyB+4Omd7hDxpf4Thrt8ImxnXeQtxzel2bClJpFQvQqK/s4oaXlkBKS7LqVLeZM+uSVIA== +vscode-windows-registry@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/vscode-windows-registry/-/vscode-windows-registry-1.0.3.tgz#377e9a8bf75c0acac81a188282a4f16f748ecd47" + integrity sha512-IXCwNAm+H5yPCn6JBz89T9AAMgy5xEN2LxbxrvHPlErmyQqCYtpCCjvisfgT2dCuaJ2T9FfiqIeIrOpDm2Jc4Q== -windows-process-tree@0.2.4: - version "0.2.4" - resolved "https://registry.yarnpkg.com/windows-process-tree/-/windows-process-tree-0.2.4.tgz#747af587b54cc6c996f2be0836cc8a8fd0dc038f" - integrity sha512-9gag9AHm3Iin/4YC1EwoIfZlqW/rG2eV7rJZ4Fy5NnAMGdewmnwsie5Rz+CJo2vSolqzzfw7hPeu3oOdniNejg== +windows-process-tree@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/windows-process-tree/-/windows-process-tree-0.3.0.tgz#cf0d9291b22fba2a7f5a687c8272866e28fbcafd" + integrity sha512-0bKI4gcd5MOsOpn2TdStCSlnjThtH6BdHrocekY9qCgTqgEtdaUs0B5BaqyzF9jXoTSwz38NMdE1F55o4fgv9Q== dependencies: nan "^2.13.2" @@ -915,15 +910,15 @@ xterm-addon-unicode11@0.3.0-beta.5: resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.3.0-beta.5.tgz#7e490799d530d3b301125c7a4e92317c161761c4" integrity sha512-SgDDL3PoMH1G48JO6T45whKAex4NPxi80UzUVitnrqyd8dFQP+oF6cxqUutULgm9HSGk62qy3mrZvIMGO5VXog== -xterm-addon-webgl@0.11.0-beta.8: - version "0.11.0-beta.8" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.11.0-beta.8.tgz#8cb4925d67c31beb8144275daf46358f42eff9fe" - integrity sha512-udRmQ/jgH8cL8VQOZweytkToIROevVeiA7WY0tIe878Wt2zKY+AYHZV8js3c1W9wHDu5G90BhmzTidJ5UwZK3Q== +xterm-addon-webgl@0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.11.1.tgz#33dd250ab52e9f51d2ff52396447962e6f53e24c" + integrity sha512-xF6DnEoV+rPtzetMBXBZVe1kLKtus7AKdEcyfq2eMHQzhaRvC+pfnU+XiCXC85kueguqu2UkBHXZs5mihK9jOQ== -xterm@4.12.0-beta.26: - version "4.12.0-beta.26" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.12.0-beta.26.tgz#57c75b732808795398a66bc1a3e06d09eaff2ada" - integrity sha512-yZB1kMBXQu2G0G1ch7TUi6f893iTZC+tmfjw/PQNZTmN46b4oX1l7rplc3sFcdrICHtmQ0Q5n1u0d6WUAdq1Kw== +xterm@4.13.0-beta.1: + version "4.13.0-beta.1" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.13.0-beta.1.tgz#ad2ad321c69a4add6e878c890f278a1da74fd7d0" + integrity sha512-gAMGqBglESTxQWph1uKVyd1jO/6eKsbicNG+Mr/YAsj06TjFVcLw839Iqu6P+DVFEV7lLLspcOb8fwX6qMBH/Q== yauzl@^2.9.2: version "2.10.0" diff --git a/resources/darwin/code.icns b/resources/darwin/code.icns index bc70a4c45194bf910ddbea8b481305afc24386e8..6c648be6ad2069f05f733c1d94441b06d56323fc 100644 GIT binary patch literal 222366 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%0Dx}MsizRy)^H*K(+y1m$jASO) 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

v7-^RD;T458c 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

BLqBk;TCJJwl-jQQx=PF#`XRy#LJT0^e1 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%0Ak9CTPWKo`SWFazr0Hte^!iu7W^Vj8BuwT^ zHLqgQ}1u}YqP{}uC$bpUC;4&Gat_T?2L9YC1O^T$T~FIUWv{}tBbM<5vde{ug) zNqiG^rIGVrF_C5}kF8|vf9qNQQ`yNA*fP@pD<_al>7iNwO!>!pw*RLz%8VPEjlbFW z8~Z~XVKRIF8NXwu_Q>r0H+%oh-hZQeU^f5E=HIE!vc|2==HJ=;J39Ly1o>yhR6p}I z7J)|MZ^cag{xj>JDgLc~;r^fX|I5bzZ2!;pzp;!xwdKtD*J=g)!*w&U3>f+Sd%0*P zyu&}vXUu=gFYLc+X8em*7aw{E<3Buq zoz_>c^yhY(`T48mqcipX!4ih&&xmEQ_)UEqr*rFrUsev;$8*!gTp50-dvD^|Zf#Sh8*@!|0>A9ZB;APrfiU zuY4Q5jH>sZT4dz?JLrJ9OKcBK&%4LE2F0vhW^L^KLr6i7tb8t7S9LnpC0lJ48asU| z0xTClwK-`z9BXX~Us|<<`R3E+W75=W$twG4dkIvyckS}|X2##YMFeR!Z<+1ZsOgD; z@>lI1UtqlVsV*Q`=JS`?Zuw#wW}@o2U3O0_v>ZS4&~`ojH@pBof9bQEUO3%9#Zl1k z`PQ|MFP!s$_s=ywFmH*Co&8%^d#A7k&E7jV*)CsT`9Swn1I(-ztyr~T+e@DOsd*tt zn)r_$&#!yxvH2Dc;T{@5np)0(^l{tuTet6i-}Cg1f;y2xG5MLvprTt;U62&Td3UG7 z7Q3gHFS3G9P)1#AV><+N!JJ1PUHSBy^_#akY=UTRiOdC(tbT{>E(1yhTfvKk@Xc)$8omQ~wu`NiQ4LKWDdg^{S_ySoY`wYfICI z|Ne&R7wY3Di#gWw7cPGEv8BtFuYhIcWEM)WeA&{+9$ma}zLll9DSQHDthxJ@KZEbje(u+2CMN&M_h-6p`Hw!oo77id PI*+sK*&g_R(*yqpzMquT diff --git a/resources/linux/code-url-handler.desktop b/resources/linux/code-url-handler.desktop index caf34337a3..b8e921920b 100644 --- a/resources/linux/code-url-handler.desktop +++ b/resources/linux/code-url-handler.desktop @@ -2,7 +2,7 @@ Name=@@NAME_LONG@@ - URL Handler Comment=Azure Data Studio GenericName=Text Editor -Exec=@@EXEC@@ --no-sandbox --open-url %U +Exec=@@EXEC@@ --open-url %U Icon=@@ICON@@ Type=Application NoDisplay=true diff --git a/resources/linux/code.desktop b/resources/linux/code.desktop index 1a3c77311d..19bd4eea44 100755 --- a/resources/linux/code.desktop +++ b/resources/linux/code.desktop @@ -2,7 +2,7 @@ Name=@@NAME_LONG@@ Comment=Data Management Tool that enables you to work with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux. GenericName=Text Editor -Exec=@@EXEC@@ --no-sandbox --unity-launch %F +Exec=@@EXEC@@ --unity-launch %F Icon=@@ICON@@ Type=Application StartupNotify=false @@ -14,5 +14,5 @@ Keywords=azuredatastudio; [Desktop Action new-empty-window] Name=New Empty Window -Exec=@@EXEC@@ --no-sandbox --new-window %F +Exec=@@EXEC@@ --new-window %F Icon=@@ICON@@ diff --git a/resources/web/code-web.js b/resources/web/code-web.js index acfa1fb8d2..78119d2a86 100644 --- a/resources/web/code-web.js +++ b/resources/web/code-web.js @@ -423,9 +423,6 @@ async function handleRoot(req, res) { const webConfigJSON = { folderUri: folderUri, staticExtensions, - settingsSyncOptions: { - enabled: args['enable-sync'] - }, webWorkerExtensionHostIframeSrc: `${SCHEME}://${secondaryHost}/static/out/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html` }; if (args['wrap-iframe']) { diff --git a/scripts/code.sh b/scripts/code.sh index 3095f3897b..5acc461f51 100755 --- a/scripts/code.sh +++ b/scripts/code.sh @@ -43,7 +43,7 @@ function code() { export ELECTRON_ENABLE_LOGGING=1 # Launch Code - exec "$CODE" . --no-sandbox "$@" + exec "$CODE" . "$@" } function code-wsl() @@ -71,7 +71,12 @@ function code-wsl() fi } -if ! [ -z ${IN_WSL+x} ]; then +if [ "$IN_WSL" == "true" ] && [ -z "$DISPLAY" ]; then code-wsl "$@" +elif [ -f /mnt/wslg/versions.txt ]; then + code --disable-gpu "$@" +else + code "$@" fi -code "$@" + +exit $? diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index 53a857ceac..b5881d1d88 100755 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -48,7 +48,7 @@ if "%INTEGRATION_TEST_ELECTRON_PATH%"=="" ( :: Tests in the extension host -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% +set ALL_PLATFORMS_API_TESTS_EXTRA_ARGS=--disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --no-sandbox --no-cached-data --disable-updates --disable-keytar --disable-extensions --disable-workspace-trust --user-data-dir=%VSCODEUSERDATADIR% :: {{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% diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index c8572aa01d..bdb7780a54 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -6,8 +6,11 @@ if [[ "$OSTYPE" == "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" + # {{SQL CARBON EDIT}} Completed disable sandboxing via --no-sandbox since we still see failures on our test runs + # --disable-setuid-sandbox: setuid sandboxes requires root and is used in containers so we disable this + # --disable-dev-shm-usage --use-gl=swiftshader: when 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 + LINUX_EXTRA_ARGS="--no-sandbox --disable-dev-shm-usage --use-gl=swiftshader" fi VSCODEUSERDATADIR=`mktemp -d 2>/dev/null` @@ -47,13 +50,6 @@ else 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 @@ -70,7 +66,7 @@ after_suite # Tests in the extension host -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" +ALL_PLATFORMS_API_TESTS_EXTRA_ARGS="--disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-keytar --disable-extensions --disable-workspace-trust --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 diff --git a/scripts/test.bat b/scripts/test.bat index 82295d8987..273e22fec0 100644 --- a/scripts/test.bat +++ b/scripts/test.bat @@ -12,7 +12,7 @@ set NAMESHORT=%NAMESHORT:"=%.exe set CODE=".build\electron\%NAMESHORT%" :: Download Electron if needed -node build\lib\electron.js +call node build\lib\electron.js if %errorlevel% neq 0 node .\node_modules\gulp\bin\gulp.js electron :: Default to only running stable tests if test grep isn't set @@ -30,7 +30,7 @@ popd endlocal :: app.exit(0) is exiting with code 255 in Electron 1.7.4. -:: See https://github.com/Microsoft/vscode/issues/28582 +:: See https://github.com/microsoft/vscode/issues/28582 echo errorlevel: %errorlevel% if %errorlevel% == 255 set errorlevel=0 exit /b %errorlevel% diff --git a/scripts/test.sh b/scripts/test.sh index 7830c232b9..dc748b9f0b 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -7,7 +7,10 @@ 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. + # {{SQL CARBON EDIT}} Completed disable sandboxing via --no-sandbox since we still see failures on our test runs + # --disable-setuid-sandbox: setuid sandboxes requires root and is used in containers so we disable this + # --disable-dev-shm-usage --use-gl=swiftshader: when 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 LINUX_EXTRA_ARGS="--no-sandbox --disable-dev-shm-usage --use-gl=swiftshader" fi diff --git a/src/bootstrap-amd.js b/src/bootstrap-amd.js index 6aaa5704d1..0be935a026 100644 --- a/src/bootstrap-amd.js +++ b/src/bootstrap-amd.js @@ -43,11 +43,11 @@ exports.load = function (entrypoint, onLoad, onError) { return; } - // cached data config - if (process.env['VSCODE_NODE_CACHED_DATA_DIR']) { + // code cache config + if (process.env['VSCODE_CODE_CACHE_PATH']) { loader.config({ nodeCachedData: { - path: process.env['VSCODE_NODE_CACHED_DATA_DIR'], + path: process.env['VSCODE_CODE_CACHE_PATH'], seed: entrypoint } }); diff --git a/src/bootstrap-node.js b/src/bootstrap-node.js index ce98104859..23ac48cc97 100644 --- a/src/bootstrap-node.js +++ b/src/bootstrap-node.js @@ -11,6 +11,7 @@ // - Posix: allow to change the current working dir via `VSCODE_CWD` if defined // - all OS: store the `process.cwd()` inside `VSCODE_CWD` for consistent lookups // TODO@bpasero revisit if chdir() on Windows is needed in the future still +// (find all users of `chdir` in code, there are more locations) function setupCurrentWorkingDirectory() { const path = require('path'); diff --git a/src/bootstrap-window.js b/src/bootstrap-window.js index 47b51aebd7..54ff75d68b 100644 --- a/src/bootstrap-window.js +++ b/src/bootstrap-window.js @@ -57,6 +57,11 @@ const configuration = await preloadGlobals.context.resolveConfiguration(); performance.mark('code/didWaitForWindowConfig'); + // Signal DOM modifications are now OK + if (typeof options?.canModifyDOM === 'function') { + options.canModifyDOM(configuration); + } + // Developer settings const { forceDisableShowDevtoolsOnError, @@ -86,13 +91,8 @@ globalThis.MonacoBootstrap.enableASARSupport(configuration.appRoot); } - // Signal DOM modifications are now OK - if (typeof options?.canModifyDOM === 'function') { - options.canModifyDOM(configuration); - } - - // Get the nls configuration into the process.env as early as possible (TODO@sandbox non-sandboxed only) - const nlsConfig = safeProcess.sandboxed ? { availableLanguages: {} } : globalThis.MonacoBootstrap.setupNLS(); + // Get the nls configuration into the process.env as early as possible + const nlsConfig = globalThis.MonacoBootstrap.setupNLS(); let locale = nlsConfig.availableLanguages['*'] || 'en'; if (locale === 'zh-tw') { @@ -152,13 +152,13 @@ 'jschardet': `../node_modules/jschardet/dist/jschardet.min.js`, }; } else { - loaderConfig.amdModulesPattern = /^(vs|sql)\//; + loaderConfig.amdModulesPattern = /^(vs|sql)\//; // {{SQL CARBON EDIT}} include sql in regex } - // Cached data config - if (configuration.nodeCachedDataDir) { + // Cached data config (node.js loading only) + if (!useCustomProtocol && configuration.codeCachePath) { loaderConfig.nodeCachedData = { - path: configuration.nodeCachedDataDir, + path: configuration.codeCachePath, seed: modulePaths.join('') }; } @@ -187,13 +187,6 @@ require(modulePaths, async result => { try { - // Wait for process environment being fully resolved - performance.mark('code/willWaitForShellEnv'); - if (!safeProcess.env['VSCODE_SKIP_PROCESS_ENV_PATCHING'] /* TODO@bpasero for https://github.com/microsoft/vscode/issues/108804 */) { - await safeProcess.shellEnv(); - } - performance.mark('code/didWaitForShellEnv'); - // Callback only after process environment is resolved const callbackResult = resultCallback(result, configuration); if (callbackResult instanceof Promise) { diff --git a/src/main.js b/src/main.js index 2c7a6f5277..f604c44a18 100644 --- a/src/main.js +++ b/src/main.js @@ -47,6 +47,9 @@ if (args['nogpu']) { // {{SQL CARBON EDIT}} const userDataPath = getUserDataPath(args); app.setPath('userData', userDataPath); +// Resolve code cache path +const codeCachePath = getCodeCachePath(); + // Configure static command line arguments const argvConfig = configureCommandlineSwitchesSync(args); @@ -78,9 +81,6 @@ protocol.registerSchemesAsPrivileged([ // Global app listeners registerListeners(); -// Cached data -const nodeCachedDataDir = getNodeCachedDir(); - /** * Support user defined locale: load it early before app('ready') * to have more things running in parallel. @@ -115,14 +115,14 @@ app.once('ready', function () { /** * Main startup routine * - * @param {string | undefined} cachedDataDir + * @param {string | undefined} codeCachePath * @param {NLSConfiguration} nlsConfig */ -function startup(cachedDataDir, nlsConfig) { +function startup(codeCachePath, nlsConfig) { nlsConfig._languagePackSupport = true; process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig); - process.env['VSCODE_NODE_CACHED_DATA_DIR'] = cachedDataDir || ''; + process.env['VSCODE_CODE_CACHE_PATH'] = codeCachePath || ''; // Load main in AMD perf.mark('code/willLoadMainBundle'); @@ -135,9 +135,9 @@ async function onReady() { perf.mark('code/mainAppReady'); try { - const [cachedDataDir, nlsConfig] = await Promise.all([nodeCachedDataDir.ensureExists(), resolveNlsConfiguration()]); + const [, nlsConfig] = await Promise.all([mkdirpIgnoreError(codeCachePath), resolveNlsConfiguration()]); - startup(cachedDataDir, nlsConfig); + startup(codeCachePath, nlsConfig); } catch (error) { console.error(error); } @@ -181,7 +181,7 @@ function configureCommandlineSwitchesSync(cliArgs) { // Read argv config const argvConfig = readArgvConfigSync(); - let browserCodeLoadingStrategy = undefined; + let browserCodeLoadingStrategy = undefined; // {{SQL CARBON EDIT}} Set to undefined by default Object.keys(argvConfig).forEach(argvKey => { const argvValue = argvConfig[argvKey]; @@ -266,7 +266,7 @@ function readArgvConfigSync() { // Fallback to default if (!argvConfig) { argvConfig = { - 'disable-color-correct-rendering': true // Force pre-Chrome-60 color profile handling (for https://github.com/Microsoft/vscode/issues/51791) + 'disable-color-correct-rendering': true // Force pre-Chrome-60 color profile handling (for https://github.com/microsoft/vscode/issues/51791) }; } @@ -300,7 +300,7 @@ function createDefaultArgvConfigSync(argvConfigPath) { ' // "disable-hardware-acceleration": true,', '', ' // Enabled by default by VS Code to resolve color issues in the renderer', - ' // See https://github.com/Microsoft/vscode/issues/51791 for details', + ' // See https://github.com/microsoft/vscode/issues/51791 for details', ' "disable-color-correct-rendering": true', '}' ]; @@ -506,46 +506,28 @@ function registerListeners() { } /** - * @returns {{ ensureExists: () => Promise }} + * @returns {string | undefined} the location to use for the code cache + * or `undefined` if disabled. */ -function getNodeCachedDir() { - return new class { +function getCodeCachePath() { - constructor() { - this.value = this.compute(); - } + // explicitly disabled via CLI args + if (process.argv.indexOf('--no-cached-data') > 0) { + return undefined; + } - async ensureExists() { - if (typeof this.value === 'string') { - try { - await mkdirp(this.value); + // running out of sources + if (process.env['VSCODE_DEV']) { + return undefined; + } - return this.value; - } catch (error) { - // ignore - } - } - } + // require commit id + const commit = product.commit; + if (!commit) { + return undefined; + } - compute() { - if (process.argv.indexOf('--no-cached-data') > 0) { - return undefined; - } - - // IEnvironmentService.isBuilt - if (process.env['VSCODE_DEV']) { - return undefined; - } - - // find commit id - const commit = product.commit; - if (!commit) { - return undefined; - } - - return path.join(userDataPath, 'CachedData', commit); - } - }; + return path.join(userDataPath, 'CachedData', commit); } /** @@ -560,6 +542,24 @@ function mkdirp(dir) { }); } +/** + * @param {string | undefined} dir + * @returns {Promise} + */ +async function mkdirpIgnoreError(dir) { + if (typeof dir === 'string') { + try { + await mkdirp(dir); + + return dir; + } catch (error) { + // ignore + } + } + + return undefined; +} + //#region NLS Support /** diff --git a/src/sql/base/browser/dom.ts b/src/sql/base/browser/dom.ts index dd627c1eff..da8636807a 100644 --- a/src/sql/base/browser/dom.ts +++ b/src/sql/base/browser/dom.ts @@ -58,7 +58,7 @@ const tabbableElementsQuerySelector = 'a[href], area[href], input:not([disabled] */ export function getFocusableElements(container: HTMLElement): HTMLElement[] { const elements = []; - container.querySelectorAll(tabbableElementsQuerySelector).forEach((element: HTMLElement) => { + container.querySelectorAll(tabbableElementsQuerySelector).forEach((element: HTMLElement) => { const style = window.getComputedStyle(element); // We should only return the elements that are visible. There are many ways to hide an element, for example setting the // visibility attribute to hidden/collapse, setting the display property to none, or if one of its ancestors is invisible. diff --git a/src/sql/base/browser/ui/buttonMenu/buttonMenu.ts b/src/sql/base/browser/ui/buttonMenu/buttonMenu.ts index ab0b2fa53d..32bb19e73a 100644 --- a/src/sql/base/browser/ui/buttonMenu/buttonMenu.ts +++ b/src/sql/base/browser/ui/buttonMenu/buttonMenu.ts @@ -8,7 +8,7 @@ import { IAction, IActionRunner } from 'vs/base/common/actions'; import { IDisposable } from 'vs/base/common/lifecycle'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; -import { append, $, addClasses } from 'vs/base/browser/dom'; +import { append, $ } from 'vs/base/browser/dom'; import { IDropdownMenuOptions, DropdownMenu, IActionProvider, ILabelRenderer } from 'vs/base/browser/ui/dropdown/dropdown'; import { IContextMenuProvider } from 'vs/base/browser/contextmenu'; import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; @@ -51,7 +51,7 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem { const labelRenderer: ILabelRenderer = (el: HTMLElement): IDisposable | null => { this.element = append(el, $('a.action-label.button-menu')); if (this.cssClass) { - addClasses(this.element, this.cssClass); + this.element.classList.add(...this.cssClass.split(' ')); } if (this.menuLabel) { this.element.innerText = this.menuLabel; diff --git a/src/sql/base/browser/ui/scrollableView/scrollableView.ts b/src/sql/base/browser/ui/scrollableView/scrollableView.ts index 26f8917e38..69d02eac4c 100644 --- a/src/sql/base/browser/ui/scrollableView/scrollableView.ts +++ b/src/sql/base/browser/ui/scrollableView/scrollableView.ts @@ -164,7 +164,7 @@ export class ScrollableView extends Disposable { for (const item of this.items) { if (item.domNode) { DOM.clearNode(item.domNode); - DOM.removeNode(item.domNode); + item.domNode.remove(); item.domNode = undefined; } dispose(item.disposables); diff --git a/src/sql/base/browser/ui/table/highPerf/cellCache.ts b/src/sql/base/browser/ui/table/highPerf/cellCache.ts index 38576b46f9..7e935b84b2 100644 --- a/src/sql/base/browser/ui/table/highPerf/cellCache.ts +++ b/src/sql/base/browser/ui/table/highPerf/cellCache.ts @@ -6,7 +6,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { ITableRenderer } from 'sql/base/browser/ui/table/highPerf/table'; -import { $, removeClass } from 'vs/base/browser/dom'; +import { $ } from 'vs/base/browser/dom'; export interface ICell { domNode: HTMLElement | null; templateData: any; @@ -64,7 +64,7 @@ export class CellCache implements IDisposable { release(cell: ICell) { const { domNode, templateId } = cell; if (domNode) { - removeClass(domNode, 'scrolling'); + domNode.classList.remove('scrolling'); removeFromParent(domNode); } diff --git a/src/sql/base/browser/ui/table/highPerf/tableView.ts b/src/sql/base/browser/ui/table/highPerf/tableView.ts index 8df68a5393..c2f4ee393f 100644 --- a/src/sql/base/browser/ui/table/highPerf/tableView.ts +++ b/src/sql/base/browser/ui/table/highPerf/tableView.ts @@ -176,7 +176,7 @@ export class TableView implements IDisposable { this.domNode.classList.add(this.domId); this.domNode.tabIndex = 0; - DOM.toggleClass(this.domNode, 'mouse-support', typeof options.mouseSupport === 'boolean' ? options.mouseSupport : true); + this.domNode.classList.toggle('mouse-support', typeof options.mouseSupport === 'boolean' ? options.mouseSupport : true); // this.ariaSetProvider = { getSetSize: (e, i, length) => length, getPosInSet: (_, index) => index + 1 }; diff --git a/src/sql/base/browser/ui/table/highPerf/tableWidget.ts b/src/sql/base/browser/ui/table/highPerf/tableWidget.ts index beadf320d4..b0ad03f0b9 100644 --- a/src/sql/base/browser/ui/table/highPerf/tableWidget.ts +++ b/src/sql/base/browser/ui/table/highPerf/tableWidget.ts @@ -157,7 +157,7 @@ class Trait implements IDisposable { constructor(private _trait: string) { } renderIndex(index: GridPosition, container: HTMLElement): void { - DOM.toggleClass(container, this._trait, this.contains(index)); + container.classList.toggle(this._trait, this.contains(index)); } unrender(container: HTMLElement): void { @@ -1035,15 +1035,15 @@ export class Table implements IDisposable { } this.view.domNode.setAttribute('role', 'tree'); - DOM.toggleClass(this.view.domNode, 'element-focused', focus.length > 0); + this.view.domNode.classList.toggle('element-focused', focus.length > 0); } private _onSelectionChange(): void { const selection = this.selection.get(); - DOM.toggleClass(this.view.domNode, 'selection-none', selection.length === 0); - DOM.toggleClass(this.view.domNode, 'selection-single', selection.length === 1); - DOM.toggleClass(this.view.domNode, 'selection-multiple', selection.length > 1); + this.view.domNode.classList.toggle('selection-none', selection.length === 0); + this.view.domNode.classList.toggle('selection-single', selection.length === 1); + this.view.domNode.classList.toggle('selection-multiple', selection.length > 1); } dispose(): void { 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 10c1e87afc..0bd9854f01 100644 --- a/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts +++ b/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts @@ -9,7 +9,7 @@ import { FilterableColumn } from 'sql/base/browser/ui/table/interfaces'; import { addDisposableListener, EventType, EventHelper, $, isAncestor, clearNode, append } from 'vs/base/browser/dom'; import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { instanceOfIDisposableDataProvider } from 'sql/base/common/dataProvider'; +import { IDisposableDataProvider, instanceOfIDisposableDataProvider } from 'sql/base/common/dataProvider'; import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; import { IInputBoxStyles, InputBox } from 'sql/base/browser/ui/inputBox/inputBox'; import { trapKeyboardNavigation } from 'sql/base/browser/dom'; @@ -242,7 +242,7 @@ export class HeaderFilter { let filterItems: Array; const dataView = this.grid.getData() as Slick.DataProvider; if (instanceOfIDisposableDataProvider(dataView)) { - filterItems = await dataView.getColumnValues(this.columnDef); + filterItems = await (dataView as IDisposableDataProvider).getColumnValues(this.columnDef); } else { const filterApplied = this.grid.getColumns().findIndex((col) => { const filterableColumn = col as FilterableColumn; @@ -467,7 +467,7 @@ export class HeaderFilter { this.hideMenu(); const dataView = this.grid.getData(); if (instanceOfIDisposableDataProvider(dataView)) { - await dataView.filter(this.grid.getColumns()); + await (dataView as IDisposableDataProvider).filter(this.grid.getColumns()); this.grid.invalidateAllRows(); this.grid.updateRowCount(); this.grid.render(); diff --git a/src/sql/platform/connection/common/connectionConfig.ts b/src/sql/platform/connection/common/connectionConfig.ts index 8ebe84889f..19b4b51d25 100644 --- a/src/sql/platform/connection/common/connectionConfig.ts +++ b/src/sql/platform/connection/common/connectionConfig.ts @@ -89,7 +89,7 @@ export class ConnectionConfig { public addConnection(profile: IConnectionProfile, matcher: ProfileMatcher = ConnectionProfile.matchesProfile): Promise { if (profile.saveProfile) { return this.addGroupFromProfile(profile).then(groupId => { - let profiles = deepClone(this.configurationService.inspect(CONNECTIONS_CONFIG_KEY).userValue); + let profiles = deepClone(this.configurationService.inspect(CONNECTIONS_CONFIG_KEY).userValue as IConnectionProfileStore[]); if (!profiles) { profiles = []; } @@ -134,7 +134,7 @@ export class ConnectionConfig { if (profile.groupId && profile.groupId !== Utils.defaultGroupId) { return Promise.resolve(profile.groupId); } else { - let groups = deepClone(this.configurationService.inspect(GROUPS_CONFIG_KEY).userValue); + let groups = deepClone(this.configurationService.inspect(GROUPS_CONFIG_KEY).userValue as IConnectionProfileGroup[]); let result = this.saveGroup(groups!, profile.groupFullName, undefined, undefined); groups = result.groups; @@ -149,7 +149,7 @@ export class ConnectionConfig { if (profileGroup.id) { return Promise.resolve(profileGroup.id); } else { - let groups = deepClone(this.configurationService.inspect(GROUPS_CONFIG_KEY).userValue); + let groups = deepClone(this.configurationService.inspect(GROUPS_CONFIG_KEY).userValue as IConnectionProfileGroup[]); let sameNameGroup = groups ? groups.find(group => group.name === profileGroup.name) : undefined; if (sameNameGroup) { let errMessage: string = nls.localize('invalidServerName', "A server group with the same name already exists."); @@ -169,9 +169,9 @@ export class ConnectionConfig { if (configs) { let fromConfig: IConnectionProfileStore[] | undefined; if (configTarget === ConfigurationTarget.USER) { - fromConfig = configs.userValue; + fromConfig = configs.userValue as IConnectionProfileStore[]; } else if (configTarget === ConfigurationTarget.WORKSPACE) { - fromConfig = configs.workspaceValue || []; + fromConfig = configs.workspaceValue as IConnectionProfileStore[] || []; } if (fromConfig) { profiles = deepClone(fromConfig); @@ -340,8 +340,8 @@ export class ConnectionConfig { * Moves the connection under the target group with the new ID. */ private changeGroupIdForConnectionInSettings(profile: ConnectionProfile, newGroupID: string, target: ConfigurationTarget = ConfigurationTarget.USER): Promise { - let profiles = deepClone(target === ConfigurationTarget.USER ? this.configurationService.inspect(CONNECTIONS_CONFIG_KEY).userValue : - this.configurationService.inspect(CONNECTIONS_CONFIG_KEY).workspaceValue); + let profiles = deepClone(target === ConfigurationTarget.USER ? this.configurationService.inspect(CONNECTIONS_CONFIG_KEY).userValue as IConnectionProfileStore[] : + this.configurationService.inspect(CONNECTIONS_CONFIG_KEY).workspaceValue as IConnectionProfileStore[]); if (profiles) { if (profile.parent && profile.parent.id === UNSAVED_GROUP_ID) { profile.groupId = newGroupID; diff --git a/src/sql/platform/connection/common/connectionProfileGroup.ts b/src/sql/platform/connection/common/connectionProfileGroup.ts index b0c3aa6ca9..2cc1689b2c 100644 --- a/src/sql/platform/connection/common/connectionProfileGroup.ts +++ b/src/sql/platform/connection/common/connectionProfileGroup.ts @@ -6,7 +6,6 @@ import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; import { Disposable } from 'vs/base/common/lifecycle'; import { isUndefinedOrNull } from 'vs/base/common/types'; -import { assign } from 'vs/base/common/objects'; export interface INewConnectionProfileGroup { id?: string; @@ -56,7 +55,7 @@ export class ConnectionProfileGroup extends Disposable implements IConnectionPro }); } - return assign({}, { name: this.name, id: this.id, parentId: this.parentId, children: subgroups, color: this.color, description: this.description }); + return Object.assign({}, { name: this.name, id: this.id, parentId: this.parentId, children: subgroups, color: this.color, description: this.description }); } public get groupName(): string { diff --git a/src/sql/platform/connection/common/connectionStatusManager.ts b/src/sql/platform/connection/common/connectionStatusManager.ts index 256bea8315..2ea5ba3718 100644 --- a/src/sql/platform/connection/common/connectionStatusManager.ts +++ b/src/sql/platform/connection/common/connectionStatusManager.ts @@ -14,7 +14,6 @@ import { join } from 'vs/base/common/path'; import * as Utils from 'sql/platform/connection/common/utils'; import * as azdata from 'azdata'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { startsWith } from 'vs/base/common/strings'; import { values } from 'vs/base/common/collections'; export class ConnectionStatusManager { @@ -193,7 +192,7 @@ export class ConnectionStatusManager { } private isSharedSession(fileUri: string): boolean { - return !!(fileUri && startsWith(fileUri, 'vsls:')); + return !!(fileUri && fileUri.startsWith('vsls:')); } public isConnected(id: string): boolean { @@ -208,7 +207,7 @@ export class ConnectionStatusManager { } public isDefaultTypeUri(uri: string): boolean { - return !!(uri && startsWith(uri, Utils.uriPrefixes.default)); + return !!(uri && uri.startsWith(Utils.uriPrefixes.default)); } public getProviderIdFromUri(ownerUri: string): string { diff --git a/src/sql/platform/connection/common/providerConnectionInfo.ts b/src/sql/platform/connection/common/providerConnectionInfo.ts index a92e3a3bad..c1fd07e52e 100644 --- a/src/sql/platform/connection/common/providerConnectionInfo.ts +++ b/src/sql/platform/connection/common/providerConnectionInfo.ts @@ -9,7 +9,6 @@ import { isString } from 'vs/base/common/types'; import * as azdata from 'azdata'; import * as Constants from 'sql/platform/connection/common/constants'; import { ICapabilitiesService, ConnectionProviderProperties } from 'sql/platform/capabilities/common/capabilitiesService'; -import { assign } from 'vs/base/common/objects'; import { ConnectionOptionSpecialType, ServiceOptionType } from 'sql/platform/connection/common/interfaces'; type SettableProperty = 'serverName' | 'authenticationType' | 'databaseName' | 'password' | 'connectionName' | 'userName'; @@ -95,7 +94,7 @@ export class ProviderConnectionInfo extends Disposable implements azdata.Connect public clone(): ProviderConnectionInfo { let instance = new ProviderConnectionInfo(this.capabilitiesService, this.providerName); - instance.options = assign({}, this.options); + instance.options = Object.assign({}, this.options); return instance; } diff --git a/src/sql/platform/connection/test/common/connectionProfile.test.ts b/src/sql/platform/connection/test/common/connectionProfile.test.ts index efcdb91cb1..7ec7c084f0 100644 --- a/src/sql/platform/connection/test/common/connectionProfile.test.ts +++ b/src/sql/platform/connection/test/common/connectionProfile.test.ts @@ -10,7 +10,6 @@ import * as assert from 'assert'; import { TestCapabilitiesService } from 'sql/platform/capabilities/test/common/testCapabilitiesService'; import { mssqlProviderName } from 'sql/platform/connection/common/constants'; import { ConnectionProviderProperties } from 'sql/platform/capabilities/common/capabilitiesService'; -import { assign } from 'vs/base/common/objects'; suite('SQL ConnectionProfileInfo tests', () => { let msSQLCapabilities: ConnectionProviderProperties; @@ -187,7 +186,7 @@ suite('SQL ConnectionProfileInfo tests', () => { }); test('createFromStoredProfile should set the id to new guid if not set in stored profile', () => { - let savedProfile: IConnectionProfileStore = assign({}, storedProfile, { id: undefined }); + let savedProfile: IConnectionProfileStore = Object.assign({}, storedProfile, { id: undefined }); let connectionProfile = ConnectionProfile.createFromStoredProfile(savedProfile, capabilitiesService); assert.equal(savedProfile.groupId, connectionProfile.groupId); assert.deepEqual(savedProfile.providerName, connectionProfile.providerName); diff --git a/src/sql/platform/connection/test/common/connectionStore.test.ts b/src/sql/platform/connection/test/common/connectionStore.test.ts index 9bba7eb96f..7214b7056d 100644 --- a/src/sql/platform/connection/test/common/connectionStore.test.ts +++ b/src/sql/platform/connection/test/common/connectionStore.test.ts @@ -13,7 +13,7 @@ import { IConnectionProfile, ConnectionOptionSpecialType, ServiceOptionType } fr import { TestConfigurationService } from 'sql/platform/connection/test/common/testConfigurationService'; import { TestCredentialsService } from 'sql/platform/credentials/test/common/testCredentialsService'; import { TestCapabilitiesService } from 'sql/platform/capabilities/test/common/testCapabilitiesService'; -import { deepClone, deepFreeze, assign } from 'vs/base/common/objects'; +import { deepClone, deepFreeze } from 'vs/base/common/objects'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { mssqlProviderName } from 'sql/platform/connection/common/constants'; import { ConnectionProviderProperties } from 'sql/platform/capabilities/common/capabilitiesService'; @@ -154,7 +154,7 @@ suite('ConnectionStore', () => { const connectionStore = new ConnectionStore(storageService, configurationService, credentialsService, capabilitiesService); for (let i = 0; i < numCreds; i++) { - const cred = assign({}, defaultNamedProfile, { serverName: defaultNamedProfile.serverName + i }); + const cred = Object.assign({}, defaultNamedProfile, { serverName: defaultNamedProfile.serverName + i }); const connectionProfile = new ConnectionProfile(capabilitiesService, cred); await connectionStore.addRecentConnection(connectionProfile); const current = connectionStore.getRecentlyUsedConnections(); @@ -190,7 +190,7 @@ suite('ConnectionStore', () => { // Then expect the only 1 instance of that connection to be listed in the MRU const connectionStore = new ConnectionStore(storageService, configurationService, credentialsService, capabilitiesService); - const cred = assign({}, defaultNamedProfile, { serverName: defaultNamedProfile.serverName + 1 }); + const cred = Object.assign({}, defaultNamedProfile, { serverName: defaultNamedProfile.serverName + 1 }); const connectionProfile = new ConnectionProfile(capabilitiesService, cred); await connectionStore.addRecentConnection(defaultNamedConnectionProfile); await connectionStore.addRecentConnection(connectionProfile); @@ -212,13 +212,13 @@ suite('ConnectionStore', () => { // Given we save 1 connection with password and multiple other connections without const connectionStore = new ConnectionStore(storageService, configurationService, credentialsService, capabilitiesService); - const integratedCred = assign({}, defaultNamedProfile, { + const integratedCred = Object.assign({}, defaultNamedProfile, { serverName: defaultNamedProfile.serverName + 'Integrated', authenticationType: 'Integrated', userName: '', password: '' }); - const noPwdCred = assign({}, defaultNamedProfile, { + const noPwdCred = Object.assign({}, defaultNamedProfile, { serverName: defaultNamedProfile.serverName + 'NoPwd', password: '' }); @@ -322,7 +322,7 @@ suite('ConnectionStore', () => { const connectionStore = new ConnectionStore(storageService, configurationService, credentialsService, capabilitiesService); - const connectionProfile: IConnectionProfile = assign({}, defaultNamedProfile, { providerName: providerName }); + const connectionProfile: IConnectionProfile = Object.assign({}, defaultNamedProfile, { providerName: providerName }); assert.ok(!connectionStore.isPasswordRequired(connectionProfile)); }); @@ -333,7 +333,7 @@ suite('ConnectionStore', () => { const credentialsService = new TestCredentialsService(); const password: string = 'asdf!@#$'; - const connectionProfile: IConnectionProfile = assign({}, defaultNamedProfile, { password }); + const connectionProfile: IConnectionProfile = Object.assign({}, defaultNamedProfile, { password }); const connectionStore = new ConnectionStore(storageService, configurationService, credentialsService, capabilitiesService); @@ -403,7 +403,7 @@ suite('ConnectionStore', () => { const profile = deepClone(defaultNamedProfile); profile.options['password'] = profile.password; profile.id = 'testId'; - let expectedProfile = assign({}, profile); + let expectedProfile = Object.assign({}, profile); expectedProfile.password = ''; expectedProfile.options['password'] = ''; expectedProfile = ConnectionProfile.fromIConnectionProfile(capabilitiesService, expectedProfile).toIConnectionProfile(); @@ -416,7 +416,7 @@ suite('ConnectionStore', () => { const configurationService = new TestConfigurationService(); const credentialsService = new TestCredentialsService(); - const profile = ConnectionProfile.fromIConnectionProfile(capabilitiesService, assign({}, defaultNamedProfile, { password: undefined })); + const profile = ConnectionProfile.fromIConnectionProfile(capabilitiesService, Object.assign({}, defaultNamedProfile, { password: undefined })); const credId = `Microsoft.SqlTools|itemtype:Profile|id:${profile.getConnectionInfoId()}`; const password: string = 'asdf!@#$'; @@ -477,7 +477,7 @@ suite('ConnectionStore', () => { credentialsService, capabilitiesService); for (let i = 0; i < 5; i++) { - const cred = assign({}, defaultNamedProfile, { serverName: defaultNamedProfile.serverName + i }); + const cred = Object.assign({}, defaultNamedProfile, { serverName: defaultNamedProfile.serverName + i }); const connectionProfile = new ConnectionProfile(capabilitiesService, cred); await connectionStore.addRecentConnection(connectionProfile); const current = connectionStore.getRecentlyUsedConnections(); @@ -485,7 +485,7 @@ suite('ConnectionStore', () => { } for (let i = 0; i < 5; i++) { - const cred = assign({}, defaultNamedProfile, { serverName: defaultNamedProfile.serverName + i }); + const cred = Object.assign({}, defaultNamedProfile, { serverName: defaultNamedProfile.serverName + i }); const connectionProfile = new ConnectionProfile(capabilitiesService, cred); connectionStore.removeRecentConnection(connectionProfile); const current = connectionStore.getRecentlyUsedConnections(); diff --git a/src/sql/platform/connection/test/common/providerConnectionInfo.test.ts b/src/sql/platform/connection/test/common/providerConnectionInfo.test.ts index 0749946f8e..58f67f841d 100644 --- a/src/sql/platform/connection/test/common/providerConnectionInfo.test.ts +++ b/src/sql/platform/connection/test/common/providerConnectionInfo.test.ts @@ -9,7 +9,6 @@ import * as azdata from 'azdata'; import * as assert from 'assert'; import { TestCapabilitiesService } from 'sql/platform/capabilities/test/common/testCapabilitiesService'; import { mssqlProviderName } from 'sql/platform/connection/common/constants'; -import { assign } from 'vs/base/common/objects'; suite('SQL ProviderConnectionInfo tests', () => { let msSQLCapabilities: any; @@ -202,7 +201,7 @@ suite('SQL ProviderConnectionInfo tests', () => { test('constructor should initialize the options given a valid model with options', () => { let options: { [key: string]: string } = {}; options['encrypt'] = 'test value'; - let conn2 = assign({}, connectionProfile, { options: options }); + let conn2 = Object.assign({}, connectionProfile, { options: options }); let conn = new ProviderConnectionInfo(capabilitiesService, conn2); assert.equal(conn.connectionName, conn2.connectionName); @@ -223,7 +222,7 @@ suite('SQL ProviderConnectionInfo tests', () => { test('getOptionsKey should create different id for different server names', () => { let conn = new ProviderConnectionInfo(capabilitiesService, connectionProfile); - let conn2 = new ProviderConnectionInfo(capabilitiesService, assign({}, connectionProfile, { serverName: connectionProfile.serverName + '1' })); + let conn2 = new ProviderConnectionInfo(capabilitiesService, Object.assign({}, connectionProfile, { serverName: connectionProfile.serverName + '1' })); assert.notEqual(conn.getOptionsKey(), conn2.getOptionsKey()); }); diff --git a/src/sql/platform/connection/test/node/connectionStatusManager.test.ts b/src/sql/platform/connection/test/node/connectionStatusManager.test.ts index 486e0aad78..fd27d110e7 100644 --- a/src/sql/platform/connection/test/node/connectionStatusManager.test.ts +++ b/src/sql/platform/connection/test/node/connectionStatusManager.test.ts @@ -15,7 +15,6 @@ import { mssqlProviderName } from 'sql/platform/connection/common/constants'; 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 { assign } from 'vs/base/common/objects'; import product from 'vs/platform/product/common/product'; let connections: ConnectionStatusManager; @@ -171,7 +170,7 @@ suite('SQL ConnectionStatusManager tests', () => { let expectedConnectionId = 'new id'; connections.addConnection(connectionProfile, connection1Id); - let updatedConnection = assign({}, connectionProfile, { groupId: expected, getOptionsKey: () => connectionProfile.getOptionsKey() + expected, id: expectedConnectionId }); + let updatedConnection = Object.assign({}, connectionProfile, { groupId: expected, getOptionsKey: () => connectionProfile.getOptionsKey() + expected, id: expectedConnectionId }); let actualId = connections.updateConnectionProfile(updatedConnection, connection1Id); let newId = Utils.generateUri(updatedConnection); @@ -246,7 +245,7 @@ suite('SQL ConnectionStatusManager tests', () => { test('getActiveConnectionProfiles should return a list of all the unique connections that the status manager knows about', () => { // Add duplicate connections - let newConnection = assign({}, connectionProfile); + let newConnection = Object.assign({}, connectionProfile); newConnection.id = 'test_id'; newConnection.serverName = 'new_server_name'; newConnection.options['databaseDisplayName'] = newConnection.databaseName; diff --git a/src/sql/platform/telemetry/common/adsTelemetryService.ts b/src/sql/platform/telemetry/common/adsTelemetryService.ts index edcda1cb0a..5980655ea2 100644 --- a/src/sql/platform/telemetry/common/adsTelemetryService.ts +++ b/src/sql/platform/telemetry/common/adsTelemetryService.ts @@ -7,7 +7,6 @@ import * as azdata from 'azdata'; import { IAdsTelemetryService, ITelemetryInfo, ITelemetryEvent, ITelemetryEventMeasures, ITelemetryEventProperties } from 'sql/platform/telemetry/common/telemetry'; import { ILogService } from 'vs/platform/log/common/log'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { assign } from 'vs/base/common/objects'; import { EventName } from 'sql/platform/telemetry/common/telemetryKeys'; @@ -36,17 +35,17 @@ class TelemetryEventImpl implements ITelemetryEvent { } public withAdditionalProperties(additionalProperties: ITelemetryEventProperties): ITelemetryEvent { - assign(this._properties, additionalProperties); + Object.assign(this._properties, additionalProperties); return this; } public withAdditionalMeasurements(additionalMeasurements: ITelemetryEventMeasures): ITelemetryEvent { - assign(this._measurements, additionalMeasurements); + Object.assign(this._measurements, additionalMeasurements); return this; } public withConnectionInfo(connectionInfo?: azdata.IConnectionProfile): ITelemetryEvent { - assign(this._properties, + Object.assign(this._properties, { authenticationType: connectionInfo?.authenticationType, provider: connectionInfo?.providerName @@ -55,7 +54,7 @@ class TelemetryEventImpl implements ITelemetryEvent { } public withServerInfo(serverInfo?: azdata.ServerInfo): ITelemetryEvent { - assign(this._properties, + Object.assign(this._properties, { connectionType: serverInfo?.isCloud !== undefined ? (serverInfo.isCloud ? 'Azure' : 'Standalone') : '', serverVersion: serverInfo?.serverVersion ?? '', diff --git a/src/sql/workbench/api/browser/mainThreadDataProtocol.ts b/src/sql/workbench/api/browser/mainThreadDataProtocol.ts index 13e8dab989..05249311bd 100644 --- a/src/sql/workbench/api/browser/mainThreadDataProtocol.ts +++ b/src/sql/workbench/api/browser/mainThreadDataProtocol.ts @@ -25,7 +25,6 @@ import { ISerializationService } from 'sql/platform/serialization/common/seriali import { IFileBrowserService } from 'sql/workbench/services/fileBrowser/common/interfaces'; import { IExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { assign } from 'vs/base/common/objects'; import { serializableToMap } from 'sql/base/common/map'; import { IAssessmentService } from 'sql/workbench/services/assessment/common/interfaces'; import { IDataGridProviderService } from 'sql/workbench/services/dataGridProvider/common/dataGridProviderService'; @@ -570,7 +569,7 @@ export class MainThreadDataProtocol extends Disposable implements MainThreadData } public $onObjectExplorerNodeExpanded(providerId: string, expandResponse: azdata.ObjectExplorerExpandInfo): void { - let expandInfo: NodeExpandInfoWithProviderId = assign({ providerId: providerId }, expandResponse); + let expandInfo: NodeExpandInfoWithProviderId = Object.assign({ providerId: providerId }, expandResponse); this._objectExplorerService.onNodeExpanded(expandInfo); } diff --git a/src/sql/workbench/api/browser/mainThreadModelViewDialog.ts b/src/sql/workbench/api/browser/mainThreadModelViewDialog.ts index 4e72250f8e..2ca833c639 100644 --- a/src/sql/workbench/api/browser/mainThreadModelViewDialog.ts +++ b/src/sql/workbench/api/browser/mainThreadModelViewDialog.ts @@ -16,7 +16,6 @@ import { ModelViewInput, ModelViewInputModel, ModeViewSaveHandler } from 'sql/wo import * as vscode from 'vscode'; import * as azdata from 'azdata'; -import { assign } from 'vs/base/common/objects'; import { TelemetryView, TelemetryAction } from 'sql/platform/telemetry/common/telemetryKeys'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; import { IEditorInput, IEditorPane } from 'vs/workbench/common/editor'; @@ -92,7 +91,7 @@ export class MainThreadModelViewDialog extends Disposable implements MainThreadM public $openDialog(handle: number, dialogName?: string): Thenable { let dialog = this.getDialog(handle); - const options = assign({}, DefaultDialogOptions); + const options = Object.assign({}, DefaultDialogOptions); options.width = dialog.width; options.dialogStyle = dialog.dialogStyle; options.dialogPosition = dialog.dialogPosition; @@ -247,7 +246,7 @@ export class MainThreadModelViewDialog extends Disposable implements MainThreadM public $openWizard(handle: number, source?: string): Thenable { let wizard = this.getWizard(handle); - const options = assign({}, DefaultWizardOptions); + const options = Object.assign({}, DefaultWizardOptions); options.width = wizard.width; this._dialogService.showWizard(wizard, options, source); return Promise.resolve(); diff --git a/src/sql/workbench/api/common/extHostModelView.ts b/src/sql/workbench/api/common/extHostModelView.ts index 727736cda8..8fca309af5 100644 --- a/src/sql/workbench/api/common/extHostModelView.ts +++ b/src/sql/workbench/api/common/extHostModelView.ts @@ -7,7 +7,7 @@ import { IMainContext } from 'vs/workbench/api/common/extHost.protocol'; import { Emitter } from 'vs/base/common/event'; -import { deepClone, assign } from 'vs/base/common/objects'; +import { deepClone } from 'vs/base/common/objects'; import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; @@ -323,17 +323,17 @@ class ComponentBuilderImpl(properties: U): azdata.ComponentBuilder { // Keep any properties that may have been set during initial object construction - this._component.properties = assign({}, this._component.properties, properties); + this._component.properties = Object.assign({}, this._component.properties, properties); return this; } withProps(properties: TPropertyBag): azdata.ComponentBuilder { - this._component.properties = assign({}, this._component.properties, properties); + this._component.properties = Object.assign({}, this._component.properties, properties); return this; } withValidation(validation: (component: T) => boolean | Thenable): azdata.ComponentBuilder { - this._component.customValidations.push(validation); + this._component.customValidations.push(validation as (component: ThisType) => boolean | Thenable); // Use specific type to avoid type assertion error return this; } @@ -406,7 +406,7 @@ class FormContainerBuilder extends GenericContainerBuilder { - this.properties = assign(this.properties, properties); + this.properties = Object.assign(this.properties, properties); return this.notifyPropertyChanged(); } @@ -803,7 +803,7 @@ class ComponentWrapper implements azdata.Component { } public updateCssStyles(cssStyles: { [key: string]: string }): Thenable { - this.properties.CSSStyles = assign(this.properties.CSSStyles || {}, cssStyles); + this.properties.CSSStyles = Object.assign(this.properties.CSSStyles || {}, cssStyles); return this.notifyPropertyChanged(); } @@ -1648,7 +1648,7 @@ class DeclarativeTableWrapper extends ComponentWrapper implements azdata.Declara // and so map them into their IDs instead. We don't want to update the actual // data property though since the caller would still expect that to contain // the Component objects they created - const properties = assign({}, this.properties); + const properties = Object.assign({}, this.properties); const componentsToAdd: ComponentWrapper[] = []; if (properties.data?.length > 0) { diff --git a/src/sql/workbench/api/common/extHostModelViewDialog.ts b/src/sql/workbench/api/common/extHostModelViewDialog.ts index f766a85ca5..c70dbd6a98 100644 --- a/src/sql/workbench/api/common/extHostModelViewDialog.ts +++ b/src/sql/workbench/api/common/extHostModelViewDialog.ts @@ -26,7 +26,7 @@ class ModelViewPanelImpl implements azdata.window.ModelViewPanel { public handle: number; protected _modelViewId: string; protected _valid: boolean = true; - protected _onValidityChanged: vscode.Event; + protected _onValidityChanged: Event; constructor(private _viewType: string, protected _extHostModelViewDialog: ExtHostModelViewDialog, diff --git a/src/sql/workbench/api/common/extHostModelViewTree.ts b/src/sql/workbench/api/common/extHostModelViewTree.ts index 9193284352..289f1670fe 100644 --- a/src/sql/workbench/api/common/extHostModelViewTree.ts +++ b/src/sql/workbench/api/common/extHostModelViewTree.ts @@ -14,7 +14,6 @@ import * as azdata from 'azdata'; import * as vsTreeExt from 'vs/workbench/api/common/extHostTreeViews'; import { Emitter } from 'vs/base/common/event'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { assign } from 'vs/base/common/objects'; import { ILogService } from 'vs/platform/log/common/log'; import { TreeDataTransferDTO } from 'vs/workbench/api/common/shared/treeDataTransfer'; @@ -166,7 +165,7 @@ export class ExtHostTreeView extends vsTreeExt.ExtHostTreeView { protected override createTreeNode(element: T, extensionTreeItem: azdata.TreeComponentItem, parent?: vsTreeExt.TreeNode | vsTreeExt.Root): vsTreeExt.TreeNode { let node = super.createTreeNode(element, extensionTreeItem, parent); if (node.item) { - node.item = assign(node.item, { checked: extensionTreeItem.checked, enabled: extensionTreeItem.enabled }); + node.item = Object.assign(node.item, { checked: extensionTreeItem.checked, enabled: extensionTreeItem.enabled }); } return node; } diff --git a/src/sql/workbench/browser/editData/editDataInput.ts b/src/sql/workbench/browser/editData/editDataInput.ts index 0de630df19..073c961e91 100644 --- a/src/sql/workbench/browser/editData/editDataInput.ts +++ b/src/sql/workbench/browser/editData/editDataInput.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorInput, IEditorInput } from 'vs/workbench/common/editor'; +import { IEditorInput } from 'vs/workbench/common/editor'; import { IConnectionManagementService, IConnectableInput, INewConnectionParams } from 'sql/platform/connection/common/connectionManagement'; import { IQueryModelService } from 'sql/workbench/services/query/common/queryModel'; import { Event, Emitter } from 'vs/base/common/event'; @@ -18,6 +18,7 @@ import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/u import { IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; import { IUntitledTextEditorModel, UntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { EncodingMode } from 'vs/workbench/services/textfile/common/textfiles'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; /** * Input for the EditDataEditor. diff --git a/src/sql/workbench/browser/editData/editDataResultsInput.ts b/src/sql/workbench/browser/editData/editDataResultsInput.ts index c1fdb3e39f..f2c5156853 100644 --- a/src/sql/workbench/browser/editData/editDataResultsInput.ts +++ b/src/sql/workbench/browser/editData/editDataResultsInput.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorInput } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; diff --git a/src/sql/workbench/browser/editor/profiler/dashboardInput.ts b/src/sql/workbench/browser/editor/profiler/dashboardInput.ts index 3fcad0eb2a..d8e6125677 100644 --- a/src/sql/workbench/browser/editor/profiler/dashboardInput.ts +++ b/src/sql/workbench/browser/editor/profiler/dashboardInput.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorInput } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IModelService } from 'vs/editor/common/services/modelService'; diff --git a/src/sql/workbench/browser/editor/profiler/profilerInput.ts b/src/sql/workbench/browser/editor/profiler/profilerInput.ts index 34dd9b0d9f..4e764f725e 100644 --- a/src/sql/workbench/browser/editor/profiler/profilerInput.ts +++ b/src/sql/workbench/browser/editor/profiler/profilerInput.ts @@ -11,7 +11,7 @@ import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import * as azdata from 'azdata'; import * as nls from 'vs/nls'; -import { EditorInput } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { Event, Emitter } from 'vs/base/common/event'; import { generateUuid } from 'vs/base/common/uuid'; diff --git a/src/sql/workbench/browser/editor/resourceViewer/resourceViewerInput.ts b/src/sql/workbench/browser/editor/resourceViewer/resourceViewerInput.ts index 7acac3d014..44916ba7ae 100644 --- a/src/sql/workbench/browser/editor/resourceViewer/resourceViewerInput.ts +++ b/src/sql/workbench/browser/editor/resourceViewer/resourceViewerInput.ts @@ -5,7 +5,7 @@ import * as azdata from 'azdata'; import * as nls from 'vs/nls'; -import { EditorInput } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { DataGridProvider, IDataGridProviderService } from 'sql/workbench/services/dataGridProvider/common/dataGridProviderService'; diff --git a/src/sql/workbench/browser/modal/dialogHelper.ts b/src/sql/workbench/browser/modal/dialogHelper.ts index 2bed853c76..85a587d66c 100644 --- a/src/sql/workbench/browser/modal/dialogHelper.ts +++ b/src/sql/workbench/browser/modal/dialogHelper.ts @@ -5,7 +5,7 @@ import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox'; import { Button } from 'sql/base/browser/ui/button/button'; -import { append, $, addClass, addClasses } from 'vs/base/browser/dom'; +import { append, $ } from 'vs/base/browser/dom'; import * as types from 'vs/base/common/types'; @@ -15,9 +15,9 @@ export function appendRow(container: HTMLElement, label: string, labelClass: str let rowContainer = append(container, $('tr')); if (rowContainerClass) { if (types.isString(rowContainerClass)) { - addClass(rowContainer, rowContainerClass); + rowContainer.classList.add(rowContainerClass); } else { - addClasses(rowContainer, ...rowContainerClass); + rowContainer.classList.add(...rowContainerClass); } } const labelContainer = append(append(rowContainer, $(`td.${labelClass}`)), $('div.dialog-label-container')); diff --git a/src/sql/workbench/browser/modal/modal.ts b/src/sql/workbench/browser/modal/modal.ts index c2585e9d4e..c33e0a612d 100644 --- a/src/sql/workbench/browser/modal/modal.ts +++ b/src/sql/workbench/browser/modal/modal.ts @@ -161,7 +161,7 @@ export abstract class Modal extends Disposable implements IThemable { * (hyoshi - 10/2/2017 tracked by https://github.com/Microsoft/carbon/issues/1836) */ public setWide(isWide: boolean): void { - DOM.toggleClass(this._bodyContainer!, 'wide', isWide); + this._bodyContainer!.classList.toggle('wide', isWide); } /** @@ -356,18 +356,18 @@ export abstract class Modal extends Disposable implements IThemable { if (this.shouldShowExpandMessageButton) { DOM.append(this._detailsButtonContainer!, this._toggleMessageDetailButton!.element); } else { - DOM.removeNode(this._toggleMessageDetailButton!.element); + this._toggleMessageDetailButton!.element.remove(); } } private toggleMessageDetail() { - const isExpanded = DOM.hasClass(this._messageSummary!, MESSAGE_EXPANDED_MODE_CLASS); - DOM.toggleClass(this._messageSummary!, MESSAGE_EXPANDED_MODE_CLASS, !isExpanded); + const isExpanded = this._messageSummary!.classList.contains(MESSAGE_EXPANDED_MODE_CLASS); + this._messageSummary!.classList.toggle(MESSAGE_EXPANDED_MODE_CLASS, !isExpanded); this._toggleMessageDetailButton!.label = isExpanded ? SHOW_DETAILS_TEXT : localize('hideMessageDetails', "Hide Details"); if (this._messageDetailText) { if (isExpanded) { - DOM.removeNode(this._messageDetail!); + this._messageDetail!.remove(); } else { DOM.append(this._messageBody!, this._messageDetail!); } @@ -576,8 +576,8 @@ export abstract class Modal extends Disposable implements IThemable { severityText = WARNING_ALT_TEXT; } levelClasses.forEach(level => { - DOM.toggleClass(this._messageIcon!, level, selectedLevel === level); - DOM.toggleClass(this._messageElement!, level, selectedLevel === level); + this._messageIcon!.classList.toggle(level, selectedLevel === level); + this._messageElement!.classList.toggle(level, selectedLevel === level); }); this._messageIcon!.title = severityText; @@ -586,7 +586,7 @@ export abstract class Modal extends Disposable implements IThemable { this._messageSummary!.title = message!; this._messageDetail!.innerText = description; } - DOM.removeNode(this._messageDetail!); + this._messageDetail!.remove(); this.messagesElementVisible = !!this._messageSummaryText; // Read out the description to screen readers so they don't have to // search around for the alert box to hear the extra information diff --git a/src/sql/workbench/browser/modelComponents/declarativeTable.component.ts b/src/sql/workbench/browser/modelComponents/declarativeTable.component.ts index 556ee78278..1830e28879 100644 --- a/src/sql/workbench/browser/modelComponents/declarativeTable.component.ts +++ b/src/sql/workbench/browser/modelComponents/declarativeTable.component.ts @@ -268,9 +268,10 @@ export default class DeclarativeTableComponent extends ContainerBase(['number', 'string', 'boolean']); - public override setProperties(properties: azdata.DeclarativeTableProperties): void { - const basicData: any[][] = properties.data ?? []; - const complexData: azdata.DeclarativeTableCellValue[][] = properties.dataValues ?? []; + public override setProperties(properties: { [key: string]: any; }): void { + let castProperties = properties as azdata.DeclarativeTableProperties; + const basicData: any[][] = castProperties.data ?? []; + const complexData: azdata.DeclarativeTableCellValue[][] = castProperties.dataValues ?? []; let finalData: azdata.DeclarativeTableCellValue[][]; finalData = basicData.map(row => { @@ -291,7 +292,7 @@ export default class DeclarativeTableComponent extends ContainerBase { diff --git a/src/sql/workbench/browser/modelComponents/diffeditor.component.ts b/src/sql/workbench/browser/modelComponents/diffeditor.component.ts index 4354970329..b4766fa484 100644 --- a/src/sql/workbench/browser/modelComponents/diffeditor.component.ts +++ b/src/sql/workbench/browser/modelComponents/diffeditor.component.ts @@ -10,7 +10,7 @@ import { import * as azdata from 'azdata'; import * as DOM from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; +import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { URI } from 'vs/base/common/uri'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -96,8 +96,8 @@ export default class DiffEditorComponent extends ComponentBase !this.multiline); } if (this._textareaContainer) { - let textAreaInputOptions = assign({}, inputOptions, { flexibleHeight: true, type: 'textarea' }); + let textAreaInputOptions = Object.assign({}, inputOptions, { flexibleHeight: true, type: 'textarea' }); this._textAreaInput = new InputBox(this._textareaContainer.nativeElement, this.contextViewService, textAreaInputOptions); this.onkeydown(this._textAreaInput.inputElement, (e: StandardKeyboardEvent) => { if (this.tryHandleKeyEvent(e)) { diff --git a/src/sql/workbench/browser/modelComponents/modelViewEditor.ts b/src/sql/workbench/browser/modelComponents/modelViewEditor.ts index 47a7f3e42c..fb2d196029 100644 --- a/src/sql/workbench/browser/modelComponents/modelViewEditor.ts +++ b/src/sql/workbench/browser/modelComponents/modelViewEditor.ts @@ -7,12 +7,13 @@ import 'vs/css!./media/modelViewEditor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; import * as DOM from 'vs/base/browser/dom'; import { ModelViewInput } from 'sql/workbench/browser/modelComponents/modelViewInput'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; export class ModelViewEditor extends EditorPane { @@ -62,7 +63,7 @@ export class ModelViewEditor extends EditorPane { } } - override async setInput(input: ModelViewInput, options?: EditorOptions, context?: IEditorOpenContext): Promise { + override async setInput(input: ModelViewInput, options?: IEditorOptions, context?: IEditorOpenContext): Promise { if (this.input && this.input.matches(input)) { return Promise.resolve(undefined); } diff --git a/src/sql/workbench/browser/modelComponents/modelViewInput.ts b/src/sql/workbench/browser/modelComponents/modelViewInput.ts index 5c9bd3e14d..522e56891b 100644 --- a/src/sql/workbench/browser/modelComponents/modelViewInput.ts +++ b/src/sql/workbench/browser/modelComponents/modelViewInput.ts @@ -6,7 +6,7 @@ import * as azdata from 'azdata'; import { IEditorModel } from 'vs/platform/editor/common/editor'; -import { EditorInput, EditorModel, IEditorInput } from 'vs/workbench/common/editor'; +import { IEditorInput } from 'vs/workbench/common/editor'; import * as DOM from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -15,6 +15,8 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { URI } from 'vs/base/common/uri'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; export type ModeViewSaveHandler = (handle: number) => Thenable; diff --git a/src/sql/workbench/browser/modelComponents/queryTextEditor.ts b/src/sql/workbench/browser/modelComponents/queryTextEditor.ts index 3bd714b3d8..13f8946c9e 100644 --- a/src/sql/workbench/browser/modelComponents/queryTextEditor.ts +++ b/src/sql/workbench/browser/modelComponents/queryTextEditor.ts @@ -6,7 +6,7 @@ import { IEditorOptions, EditorOption } 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'; +import { TextResourceEditorModel } from 'vs/workbench/common/editor/textResourceEditorModel'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { BaseTextEditor, IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'; @@ -15,13 +15,14 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; -import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; /** * Extension of TextResourceEditor that is always readonly rather than only with non UntitledInputs @@ -85,9 +86,9 @@ export class QueryTextEditor extends BaseTextEditor { return options; } - override async setInput(input: UntitledTextEditorInput, options: EditorOptions, context: IEditorOpenContext): Promise { + override async setInput(input: UntitledTextEditorInput, options: ITextEditorOptions, context: IEditorOpenContext): Promise { await super.setInput(input, options, context, CancellationToken.None); - const editorModel = await this.input.resolve() as ResourceEditorModel; + const editorModel = await this.input.resolve() as TextResourceEditorModel; await editorModel.resolve(); this.getControl().setModel(editorModel.textEditorModel); } diff --git a/src/sql/workbench/browser/modelComponents/text.component.ts b/src/sql/workbench/browser/modelComponents/text.component.ts index 9676515405..c8fc82aa25 100644 --- a/src/sql/workbench/browser/modelComponents/text.component.ts +++ b/src/sql/workbench/browser/modelComponents/text.component.ts @@ -17,8 +17,7 @@ import { Link } from 'vs/platform/opener/browser/link'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import * as DOM from 'vs/base/browser/dom'; import { ILogService } from 'vs/platform/log/common/log'; -import { attachLinkStyler } from 'vs/platform/theme/common/styler'; -import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { errorForeground } from 'vs/platform/theme/common/colorRegistry'; export enum TextType { @@ -52,8 +51,7 @@ export default class TextComponent extends TitledComponent ChangeDetectorRef)) changeRef: ChangeDetectorRef, @Inject(forwardRef(() => ElementRef)) el: ElementRef, @Inject(IInstantiationService) private instantiationService: IInstantiationService, - @Inject(ILogService) logService: ILogService, - @Inject(IThemeService) private themeService: IThemeService) { + @Inject(ILogService) logService: ILogService) { super(changeRef, el, logService); } @@ -150,7 +148,7 @@ export default class TextComponent extends TitledComponentthis.textContainer.nativeElement).appendChild(linkElement.el); // And finally update the text to remove the text up through the placeholder we just added diff --git a/src/sql/workbench/browser/modelComponents/treeComponentRenderer.ts b/src/sql/workbench/browser/modelComponents/treeComponentRenderer.ts index 379985c1ae..ef870ebdbf 100644 --- a/src/sql/workbench/browser/modelComponents/treeComponentRenderer.ts +++ b/src/sql/workbench/browser/modelComponents/treeComponentRenderer.ts @@ -144,7 +144,7 @@ export class TreeComponentRenderer extends Disposable implements IRenderer { templateData.icon.style.backgroundImage = iconUri ? `url('${iconUri.toString(true)}')` : ''; templateData.icon.style.backgroundRepeat = 'no-repeat'; templateData.icon.style.backgroundPosition = 'center'; - dom.toggleClass(templateData.icon, 'model-view-tree-node-item-icon', !!icon); + templateData.icon.classList.toggle('model-view-tree-node-item-icon', !!icon); if (element) { element.onCheckedChanged = (checked: boolean) => { this._dataProvider.onNodeCheckedChanged(element.handle, checked); diff --git a/src/sql/workbench/browser/modelComponents/viewBase.ts b/src/sql/workbench/browser/modelComponents/viewBase.ts index 59b31faf8a..480dc67bfd 100644 --- a/src/sql/workbench/browser/modelComponents/viewBase.ts +++ b/src/sql/workbench/browser/modelComponents/viewBase.ts @@ -14,7 +14,6 @@ import { Extensions, IComponentRegistry } from 'sql/platform/dashboard/browser/m import { AngularDisposable } from 'sql/base/browser/lifecycle'; import { ModelStore } from 'sql/workbench/browser/modelComponents/modelStore'; import { Event, Emitter } from 'vs/base/common/event'; -import { assign } from 'vs/base/common/objects'; import { IModelStore, IComponentDescriptor, IComponent, ModelComponentTypes } from 'sql/platform/dashboard/browser/interfaces'; import { ILogService } from 'vs/platform/log/common/log'; @@ -191,7 +190,7 @@ export abstract class ViewBase extends AngularDisposable implements IModelView { this.queueAction(componentId, (component) => { this.logService.debug(`Registering event handler for component ${componentId}`); this._register(component.registerEventHandler(e => { - let modelViewEvent: IModelViewEventArgs = assign({ + let modelViewEvent: IModelViewEventArgs = Object.assign({ componentId: componentId, isRootComponent: componentId === this.rootDescriptor.id }, e); diff --git a/src/sql/workbench/common/editor/query/queryEditorInput.ts b/src/sql/workbench/common/editor/query/queryEditorInput.ts index 834c16eef8..64d0462517 100644 --- a/src/sql/workbench/common/editor/query/queryEditorInput.ts +++ b/src/sql/workbench/common/editor/query/queryEditorInput.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { EditorInput, GroupIdentifier, IRevertOptions, ISaveOptions, IEditorInput } from 'vs/workbench/common/editor'; +import { GroupIdentifier, IRevertOptions, ISaveOptions, IEditorInput, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConnectionManagementService, IConnectableInput, INewConnectionParams, RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement'; @@ -15,10 +15,10 @@ import { QueryResultsInput } from 'sql/workbench/common/editor/query/queryResult import { IQueryModelService } from 'sql/workbench/services/query/common/queryModel'; import { ExecutionPlanOptions } from 'azdata'; -import { startsWith } from 'vs/base/common/strings'; import { IRange } from 'vs/editor/common/core/range'; import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { IQueryEditorConfiguration } from 'sql/platform/query/common/query'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; const MAX_SIZE = 13; @@ -183,8 +183,8 @@ export abstract class QueryEditorInput extends EditorInput implements IConnectab return this._text.revert(group, options); } - public override isReadonly(): boolean { - return false; + public override get capabilities(): EditorInputCapabilities { + return EditorInputCapabilities.None; } public override matches(otherInput: any): boolean { @@ -320,6 +320,6 @@ export abstract class QueryEditorInput extends EditorInput implements IConnectab } public get isSharedSession(): boolean { - return !!(this.uri && startsWith(this.uri, 'vsls:')); + return !!(this.uri && this.uri.startsWith('vsls:')); } } diff --git a/src/sql/workbench/common/editor/query/queryResultsInput.ts b/src/sql/workbench/common/editor/query/queryResultsInput.ts index b617b5824f..4232845b3c 100644 --- a/src/sql/workbench/common/editor/query/queryResultsInput.ts +++ b/src/sql/workbench/common/editor/query/queryResultsInput.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { EditorInput } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { TopOperationsState } from 'sql/workbench/common/editor/query/topOperationsState'; import { ChartState } from 'sql/workbench/common/editor/query/chartState'; diff --git a/src/sql/workbench/common/editor/query/untitledQueryEditorInput.ts b/src/sql/workbench/common/editor/query/untitledQueryEditorInput.ts index 78c4afd00f..138eb91a20 100644 --- a/src/sql/workbench/common/editor/query/untitledQueryEditorInput.ts +++ b/src/sql/workbench/common/editor/query/untitledQueryEditorInput.ts @@ -13,6 +13,7 @@ import { IResolvedTextEditorModel } from 'vs/editor/common/services/resolverServ import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { EncodingMode, IEncodingSupport } from 'vs/workbench/services/textfile/common/textfiles'; +import { EditorInputCapabilities } from 'vs/workbench/common/editor'; export class UntitledQueryEditorInput extends QueryEditorInput implements IEncodingSupport { @@ -61,8 +62,8 @@ export class UntitledQueryEditorInput extends QueryEditorInput implements IEncod return this.text.setEncoding(encoding, mode); } - override isUntitled(): boolean { + override get capabilities(): EditorInputCapabilities { // Subclasses need to explicitly opt-in to being untitled. - return true; + return EditorInputCapabilities.Untitled; } } 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 29b3c7f93d..48af54dceb 100644 --- a/src/sql/workbench/contrib/assessment/test/browser/asmtActions.test.ts +++ b/src/sql/workbench/contrib/assessment/test/browser/asmtActions.test.ts @@ -238,7 +238,8 @@ suite('Assessment Actions', () => { mtime: Date.now(), name: '', resource: fileUri, - size: 42 + size: 42, + readonly: false }); }); diff --git a/src/sql/workbench/contrib/charts/browser/actions.ts b/src/sql/workbench/contrib/charts/browser/actions.ts index 41b81a36fd..659369a34a 100644 --- a/src/sql/workbench/contrib/charts/browser/actions.ts +++ b/src/sql/workbench/contrib/charts/browser/actions.ts @@ -18,7 +18,6 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IFileDialogService, FileFilter } from 'vs/platform/dialogs/common/dialogs'; import { VSBuffer } from 'vs/base/common/buffer'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { assign } from 'vs/base/common/objects'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; @@ -55,7 +54,7 @@ export class CreateInsightAction extends Action { let queryFile = uri.fsPath; let query: string | undefined = undefined; let type: { [key: string]: any } = {}; - let options = assign({}, context.options); + let options = Object.assign({}, context.options); delete (options as any).type; type[context.options.type] = options; // create JSON diff --git a/src/sql/workbench/contrib/charts/browser/imageInsight.ts b/src/sql/workbench/contrib/charts/browser/imageInsight.ts index c8999dfd85..3beea751c3 100644 --- a/src/sql/workbench/contrib/charts/browser/imageInsight.ts +++ b/src/sql/workbench/contrib/charts/browser/imageInsight.ts @@ -9,7 +9,6 @@ import { $ } from 'vs/base/browser/dom'; import { mixin } from 'vs/base/common/objects'; import { IInsightOptions, InsightType } from 'sql/workbench/contrib/charts/common/interfaces'; import * as nls from 'vs/nls'; -import { startsWith } from 'vs/base/common/strings'; import { IInsightData } from 'sql/platform/dashboard/browser/insightRegistry'; export interface IConfig extends IInsightOptions { @@ -71,7 +70,7 @@ export class ImageInsight implements IInsight { private static _hexToBase64(hexVal: string) { - if (startsWith(hexVal, '0x')) { + if (hexVal.startsWith('0x')) { hexVal = hexVal.slice(2); } // should be able to be replaced with new Buffer(hexVal, 'hex').toString('base64') diff --git a/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts b/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts index 561d9039c8..86ff01ba96 100644 --- a/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts +++ b/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts @@ -30,8 +30,8 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { isUndefinedOrNull } from 'vs/base/common/types'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; -import { FileQueryEditorInput } from 'sql/workbench/contrib/query/common/fileQueryEditorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; +import { FileQueryEditorInput } from 'sql/workbench/contrib/query/browser/fileQueryEditorInput'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; @@ -394,7 +394,7 @@ suite('commandLineService tests', () => { querymodelService.setup(c => c.onRunQueryComplete).returns(() => Event.None); let uri = URI.file(args._[0]); const workbenchinstantiationService = workbenchInstantiationService(); - const editorInput = workbenchinstantiationService.createInstance(FileEditorInput, uri, undefined, undefined, undefined, undefined, undefined); + const editorInput = workbenchinstantiationService.createInstance(FileEditorInput, uri, undefined, undefined, undefined, undefined, undefined, undefined); const queryInput = new FileQueryEditorInput(undefined, editorInput, undefined, connectionManagementService.object, querymodelService.object, configurationService.object); queryInput.state.connected = true; const editorService: TypeMoq.Mock = TypeMoq.Mock.ofType(TestEditorService, TypeMoq.MockBehavior.Strict); diff --git a/src/sql/workbench/contrib/connection/browser/connectionStatus.ts b/src/sql/workbench/contrib/connection/browser/connectionStatus.ts index 1b2347378c..77d6aa6f89 100644 --- a/src/sql/workbench/contrib/connection/browser/connectionStatus.ts +++ b/src/sql/workbench/contrib/connection/browser/connectionStatus.ts @@ -19,6 +19,7 @@ export class ConnectionStatusbarItem extends Disposable implements IWorkbenchCon private static readonly ID = 'status.connection.status'; private statusItem: IStatusbarEntryAccessor; + private readonly name = localize('status.connection.status', "Connection Status"); constructor( @IStatusbarService private readonly statusbarService: IStatusbarService, @@ -29,11 +30,11 @@ export class ConnectionStatusbarItem extends Disposable implements IWorkbenchCon super(); this.statusItem = this._register( this.statusbarService.addEntry({ + name: this.name, text: '', ariaLabel: '' }, ConnectionStatusbarItem.ID, - localize('status.connection.status', "Connection Status"), StatusbarAlignment.RIGHT, 100) ); @@ -85,7 +86,9 @@ export class ConnectionStatusbarItem extends Disposable implements IWorkbenchCon } this.statusItem.update({ - text, ariaLabel: text, tooltip + name: this.name, + text: text, + ariaLabel: text, tooltip }); } } diff --git a/src/sql/workbench/contrib/dashboard/browser/core/dashboardPage.component.ts b/src/sql/workbench/contrib/dashboard/browser/core/dashboardPage.component.ts index 52cbf70a89..814564934d 100644 --- a/src/sql/workbench/contrib/dashboard/browser/core/dashboardPage.component.ts +++ b/src/sql/workbench/contrib/dashboard/browser/core/dashboardPage.component.ts @@ -181,7 +181,7 @@ export abstract class DashboardPage extends AngularDisposable implements IConfig let secondary: IAction[] = []; const menu = this.menuService.createMenu(MenuId.DashboardToolbar, this.contextKeyService); let groups = menu.getActions({ arg: this.connectionManagementService.connectionInfo.connectionProfile.toIConnectionProfile(), shouldForwardArgs: true }); - fillInActions(groups, { primary, secondary }, false, '', Number.MAX_SAFE_INTEGER, (action: SubmenuAction, group: string, groupSize: number) => group === undefined || group === ''); + fillInActions(groups, { primary, secondary }, false, g => g === '', Number.MAX_SAFE_INTEGER, (action: SubmenuAction, group: string, groupSize: number) => group === undefined || group === ''); primary.forEach(a => { if (a instanceof MenuItemAction) { @@ -504,7 +504,7 @@ export abstract class DashboardPage extends AngularDisposable implements IConfig return [this.propertiesWidget]; } else if (types.isArray(properties)) { return properties.map((item) => { - const retVal = objects.assign({}, this.propertiesWidget); + const retVal = Object.assign({}, this.propertiesWidget); retVal.edition = item.edition; retVal.provider = item.provider; retVal.widget = { 'properties-widget': { properties: item.properties } }; diff --git a/src/sql/workbench/contrib/dashboard/browser/dashboardEditor.ts b/src/sql/workbench/contrib/dashboard/browser/dashboardEditor.ts index 35f16338d8..cb4239f34b 100644 --- a/src/sql/workbench/contrib/dashboard/browser/dashboardEditor.ts +++ b/src/sql/workbench/contrib/dashboard/browser/dashboardEditor.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; @@ -24,6 +24,7 @@ import { IConnectionManagementService } from 'sql/platform/connection/common/con import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IQueryManagementService } from 'sql/workbench/services/query/common/queryManagement'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; export class DashboardEditor extends EditorPane { @@ -77,7 +78,7 @@ export class DashboardEditor extends EditorPane { this._dashboardService.layout(dimension); } - public override async setInput(input: DashboardInput, options: EditorOptions, context: IEditorOpenContext): Promise { + public override async setInput(input: DashboardInput, options: IEditorOptions, context: IEditorOpenContext): Promise { if (this.input && this.input.matches(input)) { return Promise.resolve(undefined); } diff --git a/src/sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/chartInsight.component.ts b/src/sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/chartInsight.component.ts index 624fffec3c..ffb72a90da 100644 --- a/src/sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/chartInsight.component.ts +++ b/src/sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/chartInsight.component.ts @@ -19,7 +19,6 @@ import { IThemeService, IColorTheme } from 'vs/platform/theme/common/themeServic import { IPointDataSet } from 'sql/workbench/contrib/charts/browser/interfaces'; import { IInsightsView, IInsightData } from 'sql/platform/dashboard/browser/insightRegistry'; import { ChartType, LegendPosition } from 'sql/workbench/contrib/charts/common/interfaces'; -import { createMemoizer } from 'vs/base/common/decorators'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; @Component({ @@ -35,8 +34,6 @@ import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; ` }) export abstract class ChartInsight extends Disposable implements IInsightsView { - protected static readonly MEMOIZER = createMemoizer(); - private _isDataAvailable: boolean = false; protected _hasInit: boolean = false; protected _hasError: boolean = false; @@ -132,7 +129,7 @@ export abstract class ChartInsight extends Disposable implements IInsightsView { @Input() set data(data: IInsightData) { // unmemoize chart data as the data needs to be recalced - ChartInsight.MEMOIZER.clear(); + this.clearMemoize(); this._data = this.filterToTopNData(data); if (isValidData(data)) { this._isDataAvailable = true; @@ -170,8 +167,9 @@ export abstract class ChartInsight extends Disposable implements IInsightsView { } protected clearMemoize(): void { - // unmemoize getters since their result can be changed by a new config - ChartInsight.MEMOIZER.clear(); + this._cachedChartData = undefined; + this._cachedColors = undefined; + this._cachedLabels = undefined; } public setConfig(config: IChartConfig) { @@ -185,77 +183,85 @@ export abstract class ChartInsight extends Disposable implements IInsightsView { } /* Typescript does not allow you to access getters/setters for super classes. - his is a workaround that allows us to still call base getter */ - @ChartInsight.MEMOIZER + his is a workaround that allows us to still call base getter */ + private _cachedChartData: Array; protected getChartData(): Array { - if (this._config.dataDirection === 'horizontal') { - if (this._config.labelFirstColumn) { - return this._data.rows.map((row) => { - return { - data: row.map(item => Number(item)).slice(1), - label: row[0] - }; - }); + if (!this._cachedChartData) { + if (this._config.dataDirection === 'horizontal') { + if (this._config.labelFirstColumn) { + this._cachedChartData = this._data.rows.map((row) => { + return { + data: row.map(item => Number(item)).slice(1), + label: row[0] + }; + }); + } else { + this._cachedChartData = this._data.rows.map((row, i) => { + return { + data: row.map(item => Number(item)), + label: 'Series' + i + }; + }); + } } else { - return this._data.rows.map((row, i) => { - return { - data: row.map(item => Number(item)), - label: 'Series' + i - }; - }); - } - } else { - if (this._config.columnsAsLabels) { - return this._data.rows[0].slice(1).map((row, i) => { - return { - data: this._data.rows.map(row => Number(row[i + 1])), - label: this._data.columns[i + 1] - }; - }); - } else { - return this._data.rows[0].slice(1).map((row, i) => { - return { - data: this._data.rows.map(row => Number(row[i + 1])), - label: 'Series' + (i + 1) - }; - }); + if (this._config.columnsAsLabels) { + this._cachedChartData = this._data.rows[0].slice(1).map((row, i) => { + return { + data: this._data.rows.map(row => Number(row[i + 1])), + label: this._data.columns[i + 1] + }; + }); + } else { + this._cachedChartData = this._data.rows[0].slice(1).map((row, i) => { + return { + data: this._data.rows.map(row => Number(row[i + 1])), + label: 'Series' + (i + 1) + }; + }); + } } } + return this._cachedChartData; } public get chartData(): Array { return this.getChartData(); } - @ChartInsight.MEMOIZER + private _cachedLabels: Array; public getLabels(): Array { - if (this._config.dataDirection === 'horizontal') { - if (this._config.labelFirstColumn) { - return this._data.columns.slice(1); + if (!this._cachedLabels) { + if (this._config.dataDirection === 'horizontal') { + if (this._config.labelFirstColumn) { + this._cachedLabels = this._data.columns.slice(1); + } else { + this._cachedLabels = this._data.columns; + } } else { - return this._data.columns; + this._cachedLabels = this._data.rows.map(row => row[0]); } - } else { - return this._data.rows.map(row => row[0]); } + return this._cachedLabels; } public get labels(): Array { return this.getLabels(); } - - @ChartInsight.MEMOIZER + private _cachedColors: { backgroundColor: string[] }[]; public get colors(): { backgroundColor: string[] }[] { - if (this._config && this._config.colorMap) { - const backgroundColor = this.labels.map((item) => { - return this._config.colorMap[item]; - }); - const colorsMap = { backgroundColor }; - return [colorsMap]; - } else { - return undefined; + if (!this._cachedColors) { + if (this._config && this._config.colorMap) { + const backgroundColor = this.labels.map((item) => { + return this._config.colorMap[item]; + }); + const colorsMap = { backgroundColor }; + this._cachedColors = [colorsMap]; + } else { + this._cachedColors = undefined; + } } + return this._cachedColors; } public set legendPosition(input: LegendPosition) { diff --git a/src/sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/types/lineChart.component.ts b/src/sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/types/lineChart.component.ts index db6f5443c0..75468e65d5 100644 --- a/src/sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/types/lineChart.component.ts +++ b/src/sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/types/lineChart.component.ts @@ -50,22 +50,25 @@ export default class LineChart extends BarChart { protected override clearMemoize() { super.clearMemoize(); - LineChart.MEMOIZER.clear(); + this._cachedPointData = undefined; } - @LineChart.MEMOIZER + private _cachedPointData: Array; protected getDataAsPoint(): Array { - const dataSetMap: { [label: string]: IPointDataSet } = {}; - this._data.rows.map(row => { - if (row && row.length >= 3) { - const legend = row[0]; - if (!dataSetMap[legend]) { - dataSetMap[legend] = { label: legend, data: [], fill: false }; + if (!this._cachedPointData) { + const dataSetMap: { [label: string]: IPointDataSet } = {}; + this._data.rows.map(row => { + if (row && row.length >= 3) { + const legend = row[0]; + if (!dataSetMap[legend]) { + dataSetMap[legend] = { label: legend, data: [], fill: false }; + } + dataSetMap[legend].data.push({ x: Number(row[1]), y: Number(row[2]) }); } - dataSetMap[legend].data.push({ x: Number(row[1]), y: Number(row[2]) }); - } - }); - return values(dataSetMap); + }); + this._cachedPointData = values(dataSetMap); + } + return this._cachedPointData; } public override get labels(): Array { diff --git a/src/sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/types/timeSeriesChart.component.ts b/src/sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/types/timeSeriesChart.component.ts index 2bf3678bac..a23d57988d 100644 --- a/src/sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/types/timeSeriesChart.component.ts +++ b/src/sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/types/timeSeriesChart.component.ts @@ -6,7 +6,7 @@ import LineChart, { ILineConfig } from './lineChart.component'; import { defaultChartConfig } from 'sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/interfaces'; -import { mixin, deepClone, assign } from 'vs/base/common/objects'; +import { mixin, deepClone } from 'vs/base/common/objects'; import { Color } from 'vs/base/common/color'; import { ChangeDetectorRef, Inject, forwardRef } from '@angular/core'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -58,7 +58,7 @@ export default class TimeSeriesChart extends LineChart { } }; - this.options = assign({}, mixin(this.options, options)); + this.options = Object.assign({}, mixin(this.options, options)); } protected override getDataAsPoint(): Array { diff --git a/src/sql/workbench/contrib/dashboard/browser/widgets/insights/views/imageInsight.component.ts b/src/sql/workbench/contrib/dashboard/browser/widgets/insights/views/imageInsight.component.ts index edc6dfedc7..a05be8f2e6 100644 --- a/src/sql/workbench/contrib/dashboard/browser/widgets/insights/views/imageInsight.component.ts +++ b/src/sql/workbench/contrib/dashboard/browser/widgets/insights/views/imageInsight.component.ts @@ -7,7 +7,6 @@ import { Component, Input, Inject, ChangeDetectorRef, forwardRef, ViewChild, OnI import { mixin } from 'vs/base/common/objects'; import { IInsightsView, IInsightData } from 'sql/platform/dashboard/browser/insightRegistry'; -import { startsWith } from 'vs/base/common/strings'; interface IConfig { encoding?: string; @@ -70,7 +69,7 @@ export default class ImageInsight implements IInsightsView, OnInit { private static _hexToBase64(hexVal: string) { - if (startsWith(hexVal, '0x')) { + if (hexVal.startsWith('0x')) { hexVal = hexVal.slice(2); } // should be able to be replaced with new Buffer(hexVal, 'hex').toString('base64') diff --git a/src/sql/workbench/contrib/dataExplorer/browser/connectionViewletPanel.ts b/src/sql/workbench/contrib/dataExplorer/browser/connectionViewletPanel.ts index 4a08626d95..e45e38c257 100644 --- a/src/sql/workbench/contrib/dataExplorer/browser/connectionViewletPanel.ts +++ b/src/sql/workbench/contrib/dataExplorer/browser/connectionViewletPanel.ts @@ -74,7 +74,7 @@ export class ConnectionViewletPanel extends ViewPane { override layoutBody(size: number): void { this._serverTreeView.layout(size); - DOM.toggleClass(this._root!, 'narrow', this._root!.clientWidth < 300); + this._root!.classList.toggle('narrow', this._root!.clientWidth < 300); } show(): void { diff --git a/src/sql/workbench/contrib/dataExplorer/browser/dataExplorerViewlet.ts b/src/sql/workbench/contrib/dataExplorer/browser/dataExplorerViewlet.ts index 576af6c30d..426c04c812 100644 --- a/src/sql/workbench/contrib/dataExplorer/browser/dataExplorerViewlet.ts +++ b/src/sql/workbench/contrib/dataExplorer/browser/dataExplorerViewlet.ts @@ -5,7 +5,7 @@ import { localize } from 'vs/nls'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { toggleClass, Dimension } from 'vs/base/browser/dom'; +import { Dimension } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -80,7 +80,7 @@ export class DataExplorerViewPaneContainer extends ViewPaneContainer { } override layout(dimension: Dimension): void { - toggleClass(this.root!, 'narrow', dimension.width <= 300); + this.root!.classList.toggle('narrow', dimension.width <= 300); super.layout(new Dimension(dimension.width, dimension.height)); } diff --git a/src/sql/workbench/contrib/editData/browser/editDataEditor.ts b/src/sql/workbench/contrib/editData/browser/editDataEditor.ts index ade84a4c92..756be49b8e 100644 --- a/src/sql/workbench/contrib/editData/browser/editDataEditor.ts +++ b/src/sql/workbench/contrib/editData/browser/editDataEditor.ts @@ -7,7 +7,7 @@ import * as strings from 'vs/base/common/strings'; import * as DOM from 'vs/base/browser/dom'; import * as nls from 'vs/nls'; -import { EditorOptions, EditorInput, IEditorControl, IEditorPane, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorControl, IEditorPane, IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -35,9 +35,12 @@ import { EditDataResultsEditor } from 'sql/workbench/contrib/editData/browser/ed import { EditDataResultsInput } from 'sql/workbench/browser/editData/editDataResultsInput'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { DisposableStore } from 'vs/base/common/lifecycle'; /** * Editor that hosts an action bar and a resultSetInput for an edit data session @@ -71,6 +74,7 @@ export class EditDataEditor extends EditorPane { private _queryEditorVisible: IContextKey; private hideQueryResultsView = false; + private readonly _disposables = new DisposableStore(); constructor( @ITelemetryService _telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @@ -79,7 +83,8 @@ export class EditDataEditor extends EditorPane { @IQueryModelService private _queryModelService: IQueryModelService, @IEditorDescriptorService private _editorDescriptorService: IEditorDescriptorService, @IContextKeyService contextKeyService: IContextKeyService, - @IStorageService storageService: IStorageService + @IStorageService storageService: IStorageService, + @IEditorGroupsService editorGroupsService: IEditorGroupsService ) { super(EditDataEditor.ID, _telemetryService, themeService, storageService); @@ -87,18 +92,28 @@ export class EditDataEditor extends EditorPane { this._queryEditorVisible = queryContext.QueryEditorVisibleContext.bindTo(contextKeyService); } - if (_editorService) { - _editorService.overrideOpenEditor({ - open: (editor, options, group) => { - if (this.isVisible() && (editor !== this.input || group !== this.group)) { - this.saveEditorViewState(); - } - return {}; - } - }); + if (editorGroupsService) { + // Add all the initial groups to be listened to + editorGroupsService.whenReady.then(() => editorGroupsService.groups.forEach(group => { + this.registerGroupListener(group); + })); + + // Additional groups added should also be listened to + this._register(editorGroupsService.onDidAddGroup((group) => this.registerGroupListener(group))); + + this._register(this._disposables); } } + private registerGroupListener(group: IEditorGroup): void { + const listener = group.onWillOpenEditor(e => { + if (this.isVisible() && (e.editor !== this.input || group !== this.group)) { + this.saveEditorViewState(); + } + }); + this._disposables.add(listener); + } + // PUBLIC METHODS //////////////////////////////////////////////////////////// // Getters and Setters @@ -214,7 +229,7 @@ export class EditDataEditor extends EditorPane { /** * Sets the input data for this editor. */ - public override setInput(newInput: EditDataInput, options?: EditorOptions, context?: IEditorOpenContext): Promise { + public override setInput(newInput: EditDataInput, options?: IEditorOptions, context?: IEditorOpenContext): Promise { let oldInput = this.input; if (!newInput.setup) { this._initialized = false; @@ -492,7 +507,7 @@ export class EditDataEditor extends EditorPane { /** * Sets input for the results editor after it has been created. */ - private _onResultsEditorCreated(resultsEditor: EditDataResultsEditor, resultsInput: EditDataResultsInput, options: EditorOptions): Promise { + private _onResultsEditorCreated(resultsEditor: EditDataResultsEditor, resultsInput: EditDataResultsInput, options: IEditorOptions): Promise { this._resultsEditor = resultsEditor; return this._resultsEditor.setInput(resultsInput, options, undefined); } @@ -500,7 +515,7 @@ export class EditDataEditor extends EditorPane { /** * Sets input for the SQL editor after it has been created. */ - private _onSqlEditorCreated(sqlEditor: TextResourceEditor, sqlInput: UntitledTextEditorInput, options: EditorOptions): Thenable { + private _onSqlEditorCreated(sqlEditor: TextResourceEditor, sqlInput: UntitledTextEditorInput, options: IEditorOptions): Thenable { this._sqlEditor = sqlEditor; return this._sqlEditor.setInput(sqlInput, options, undefined, CancellationToken.None); } @@ -520,7 +535,7 @@ export class EditDataEditor extends EditorPane { * - Opened for the first time * - Opened with a new EditDataInput */ - private _setNewInput(newInput: EditDataInput, options?: EditorOptions): Promise { + private _setNewInput(newInput: EditDataInput, options?: IEditorOptions): Promise { // Promises that will ensure proper ordering of editor creation logic let createEditors: () => Promise; @@ -606,7 +621,7 @@ export class EditDataEditor extends EditorPane { * Handles setting input for this editor. If this new input does not match the old input (e.g. a new file * has been opened with the same editor, or we are opening the editor for the first time). */ - private _updateInput(oldInput: EditDataInput, newInput: EditDataInput, options?: EditorOptions): Promise { + private _updateInput(oldInput: EditDataInput, newInput: EditDataInput, options?: IEditorOptions): Promise { if (this._sqlEditor) { this._sqlEditor.clearInput(); } diff --git a/src/sql/workbench/contrib/editData/browser/editDataGridPanel.ts b/src/sql/workbench/contrib/editData/browser/editDataGridPanel.ts index bd5e42b43e..a8edd56fe8 100644 --- a/src/sql/workbench/contrib/editData/browser/editDataGridPanel.ts +++ b/src/sql/workbench/contrib/editData/browser/editDataGridPanel.ts @@ -30,7 +30,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EditUpdateCellResult } from 'azdata'; import { ILogService } from 'vs/platform/log/common/log'; -import { deepClone, assign } from 'vs/base/common/objects'; +import { deepClone } from 'vs/base/common/objects'; import { Event } from 'vs/base/common/event'; import { equals } from 'vs/base/common/arrays'; import * as DOM from 'vs/base/browser/dom'; @@ -362,7 +362,7 @@ export class EditDataGridPanel extends GridParentComponent { async handleResultSet(self: EditDataGridPanel, event: any): Promise { // Clone the data before altering it to avoid impacting other subscribers - let resultSet = assign({}, event.data); + let resultSet = Object.assign({}, event.data); if (!resultSet.complete) { return; } diff --git a/src/sql/workbench/contrib/editData/browser/editDataResultsEditor.ts b/src/sql/workbench/contrib/editData/browser/editDataResultsEditor.ts index ffe947f4f2..17ab19ce5f 100644 --- a/src/sql/workbench/contrib/editData/browser/editDataResultsEditor.ts +++ b/src/sql/workbench/contrib/editData/browser/editDataResultsEditor.ts @@ -5,7 +5,7 @@ import * as DOM from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { getZoomLevel } from 'vs/base/browser/browser'; import { Configuration } from 'vs/editor/browser/config/configuration'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -20,6 +20,7 @@ import { EditDataGridPanel } from 'sql/workbench/contrib/editData/browser/editDa import { EditDataResultsInput } from 'sql/workbench/browser/editData/editDataResultsInput'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; export class EditDataResultsEditor extends EditorPane { @@ -63,7 +64,7 @@ export class EditDataResultsEditor extends EditorPane { public layout(dimension: DOM.Dimension): void { } - public override setInput(input: EditDataResultsInput, options: EditorOptions, context: IEditorOpenContext): Promise { + public override setInput(input: EditDataResultsInput, options: IEditorOptions, context: IEditorOpenContext): Promise { super.setInput(input, options, context, CancellationToken.None); this._applySettings(); if (!input.hasBootstrapped) { diff --git a/src/sql/workbench/contrib/notebook/browser/find/notebookFindDecorations.ts b/src/sql/workbench/contrib/notebook/browser/find/notebookFindDecorations.ts index 736ad92c6a..98694166a8 100644 --- a/src/sql/workbench/contrib/notebook/browser/find/notebookFindDecorations.ts +++ b/src/sql/workbench/contrib/notebook/browser/find/notebookFindDecorations.ts @@ -318,6 +318,7 @@ export class NotebookFindDecorations implements IDisposable { } private static readonly _CURRENT_FIND_MATCH_DECORATION = ModelDecorationOptions.register({ + description: 'CURRENT_FIND_MATCH_DECORATION', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, zIndex: 13, className: 'currentFindMatch', @@ -333,6 +334,7 @@ export class NotebookFindDecorations implements IDisposable { }); private static readonly _FIND_MATCH_DECORATION = ModelDecorationOptions.register({ + description: 'FIND_MATCH_DECORATION', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'findMatch', showIfCollapsed: true, @@ -347,12 +349,14 @@ export class NotebookFindDecorations implements IDisposable { }); private static readonly _FIND_MATCH_NO_OVERVIEW_DECORATION = ModelDecorationOptions.register({ + description: 'FIND_MATCH_NO_OVERVIEW_DECORATION', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'findMatch', showIfCollapsed: true }); private static readonly _FIND_MATCH_ONLY_OVERVIEW_DECORATION = ModelDecorationOptions.register({ + description: 'FIND_MATCH_ONLY_OVERVIEW_DECORATION', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, overviewRuler: { color: themeColorFromId(overviewRulerFindMatchForeground), @@ -361,12 +365,14 @@ export class NotebookFindDecorations implements IDisposable { }); private static readonly _RANGE_HIGHLIGHT_DECORATION = ModelDecorationOptions.register({ + description: 'RANGE_HIGHLIGHT_DECORATION', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'rangeHighlight', isWholeLine: false }); private static readonly _FIND_SCOPE_DECORATION = ModelDecorationOptions.register({ + description: 'FIND_SCOPE_DECORATION', className: 'findScope', isWholeLine: true }); diff --git a/src/sql/workbench/contrib/notebook/browser/find/notebookFindWidget.ts b/src/sql/workbench/contrib/notebook/browser/find/notebookFindWidget.ts index 884afbfd5a..741e0640a7 100644 --- a/src/sql/workbench/contrib/notebook/browser/find/notebookFindWidget.ts +++ b/src/sql/workbench/contrib/notebook/browser/find/notebookFindWidget.ts @@ -130,9 +130,9 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL if (FIND_WIDGET_INITIAL_WIDTH + 28 + minimapWidth - MAX_MATCHES_COUNT_WIDTH >= editorWidth + 50) { collapsedFindWidget = true; } - dom.toggleClass(this._domNode, 'collapsed-find-widget', collapsedFindWidget); - dom.toggleClass(this._domNode, 'narrow-find-widget', narrowFindWidget); - dom.toggleClass(this._domNode, 'reduced-find-widget', reducedFindWidget); + this._domNode.classList.toggle('collapsed-find-widget', collapsedFindWidget); + this._domNode.classList.toggle('narrow-find-widget', narrowFindWidget); + this._domNode.classList.toggle('reduced-find-widget', reducedFindWidget); if (!narrowFindWidget && !collapsedFindWidget) { // the minimal left offset of findwidget is 15px. @@ -215,7 +215,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL } if (e.searchString || e.matchesCount || e.matchesPosition) { let showRedOutline = (this._state.searchString.length > 0 && this._state.matchesCount === 0); - dom.toggleClass(this._domNode, 'no-results', showRedOutline); + this._domNode.classList.toggle('no-results', showRedOutline); this._updateMatchesCount(); } @@ -568,7 +568,7 @@ class SimpleButton extends Widget { } public setEnabled(enabled: boolean): void { - dom.toggleClass(this._domNode, 'disabled', !enabled); + this._domNode.classList.toggle('disabled', !enabled); this._domNode.setAttribute('aria-disabled', String(!enabled)); this._domNode.tabIndex = enabled ? 0 : -1; } @@ -578,6 +578,6 @@ class SimpleButton extends Widget { } public toggleClass(className: string, shouldHaveIt: boolean): void { - dom.toggleClass(this._domNode, className, shouldHaveIt); + this._domNode.classList.toggle(className, shouldHaveIt); } } diff --git a/src/sql/workbench/contrib/notebook/browser/models/diffNotebookInput.ts b/src/sql/workbench/contrib/notebook/browser/models/diffNotebookInput.ts index afd77366ea..9fbcf5b419 100644 --- a/src/sql/workbench/contrib/notebook/browser/models/diffNotebookInput.ts +++ b/src/sql/workbench/contrib/notebook/browser/models/diffNotebookInput.ts @@ -3,8 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; -import { SideBySideEditorInput } from 'vs/workbench/common/editor'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { FileNotebookInput } from 'sql/workbench/contrib/notebook/browser/models/fileNotebookInput'; diff --git a/src/sql/workbench/contrib/notebook/browser/models/fileNotebookInput.ts b/src/sql/workbench/contrib/notebook/browser/models/fileNotebookInput.ts index be118f030c..aa57ad71a1 100644 --- a/src/sql/workbench/contrib/notebook/browser/models/fileNotebookInput.ts +++ b/src/sql/workbench/contrib/notebook/browser/models/fileNotebookInput.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { URI } from 'vs/base/common/uri'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts b/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts index 8aba32e7a6..7c8d67a4bb 100644 --- a/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts +++ b/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorInput, EditorModel, IRevertOptions, GroupIdentifier, IEditorInput } from 'vs/workbench/common/editor'; +import { IRevertOptions, GroupIdentifier, IEditorInput, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { Emitter, Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import * as resources from 'vs/base/common/resources'; @@ -12,7 +12,7 @@ import * as azdata from 'azdata'; import { IStandardKernelWithProvider, getProvidersForFileName, getStandardKernelsForProvider } from 'sql/workbench/services/notebook/browser/models/notebookUtils'; import { INotebookService, DEFAULT_NOTEBOOK_PROVIDER, IProviderInfo } from 'sql/workbench/services/notebook/browser/notebookService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { INotebookModel, IContentManager, NotebookContentChange } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { Schemas } from 'vs/base/common/network'; @@ -25,16 +25,18 @@ import { NotebookChangeType } from 'sql/workbench/services/notebook/common/contr import { Deferred } from 'sql/base/common/promise'; import { NotebookTextFileModel } from 'sql/workbench/contrib/notebook/browser/models/notebookTextFileModel'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; -import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; +import { TextResourceEditorModel } from 'vs/workbench/common/editor/textResourceEditorModel'; import { UntitledTextEditorModel, IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { AbstractResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { NotebookFindModel } from 'sql/workbench/contrib/notebook/browser/find/notebookFindModel'; import { onUnexpectedError } from 'vs/base/common/errors'; import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel'; import { INotebookInput } from 'sql/workbench/services/notebook/browser/interface'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; export type ModeViewSaveHandler = (handle: number) => Thenable; @@ -46,7 +48,7 @@ export class NotebookEditorModel extends EditorModel { private _lastEditFullReplacement: boolean; private _isFirstKernelChange: boolean = true; constructor(public readonly notebookUri: URI, - private textEditorModel: ITextFileEditorModel | IUntitledTextEditorModel | ResourceEditorModel, + private textEditorModel: ITextFileEditorModel | IUntitledTextEditorModel | TextResourceEditorModel, @INotebookService private notebookService: INotebookService, @ITextResourcePropertiesService private textResourcePropertiesService: ITextResourcePropertiesService ) { @@ -72,18 +74,18 @@ export class NotebookEditorModel extends EditorModel { })); if (this.textEditorModel instanceof UntitledTextEditorModel) { this._register(this.textEditorModel.onDidChangeDirty(e => { - let dirty = this.textEditorModel instanceof ResourceEditorModel ? false : this.textEditorModel.isDirty(); + let dirty = this.textEditorModel instanceof TextResourceEditorModel ? false : this.textEditorModel.isDirty(); this.setDirty(dirty); })); } else { if (this.textEditorModel instanceof TextFileEditorModel) { this._register(this.textEditorModel.onDidSave(() => { - let dirty = this.textEditorModel instanceof ResourceEditorModel ? false : this.textEditorModel.isDirty(); + let dirty = this.textEditorModel instanceof TextResourceEditorModel ? false : this.textEditorModel.isDirty(); this.setDirty(dirty); this.sendNotebookSerializationStateChange(); })); this._register(this.textEditorModel.onDidChangeDirty(() => { - let dirty = this.textEditorModel instanceof ResourceEditorModel ? false : this.textEditorModel.isDirty(); + let dirty = this.textEditorModel instanceof TextResourceEditorModel ? false : this.textEditorModel.isDirty(); this.setDirty(dirty); })); this._register(this.textEditorModel.onDidResolve(async (e) => { @@ -94,7 +96,7 @@ export class NotebookEditorModel extends EditorModel { })); } } - this._dirty = this.textEditorModel instanceof ResourceEditorModel ? false : this.textEditorModel.isDirty(); + this._dirty = this.textEditorModel instanceof TextResourceEditorModel ? false : this.textEditorModel.isDirty(); } public get contentString(): string { @@ -107,7 +109,7 @@ export class NotebookEditorModel extends EditorModel { } isDirty(): boolean { - return this.textEditorModel instanceof ResourceEditorModel ? false : this.textEditorModel.isDirty(); + return this.textEditorModel instanceof TextResourceEditorModel ? false : this.textEditorModel.isDirty(); } public setDirty(dirty: boolean): void { @@ -208,7 +210,7 @@ export class NotebookEditorModel extends EditorModel { } } -type TextInput = ResourceEditorInput | UntitledTextEditorInput | FileEditorInput; +type TextInput = AbstractResourceEditorInput | UntitledTextEditorInput | FileEditorInput; export abstract class NotebookInput extends EditorInput implements INotebookInput { private _providerId: string; @@ -286,8 +288,8 @@ export abstract class NotebookInput extends EditorInput implements INotebookInpu return this._title; } - public override isReadonly(): boolean { - return false; + public override get capabilities(): EditorInputCapabilities { + return EditorInputCapabilities.None; } public async getProviderInfo(): Promise { @@ -384,7 +386,7 @@ export abstract class NotebookInput extends EditorInput implements INotebookInpu if (this._model) { return Promise.resolve(this._model); } else { - let textOrUntitledEditorModel: ITextFileEditorModel | IUntitledTextEditorModel | ResourceEditorModel; + let textOrUntitledEditorModel: ITextFileEditorModel | IUntitledTextEditorModel | TextResourceEditorModel; if (this.resource.scheme === Schemas.untitled) { if (this._untitledEditorModel) { this._untitledEditorModel.textEditorModel.onBeforeAttached(); @@ -392,15 +394,15 @@ export abstract class NotebookInput extends EditorInput implements INotebookInpu } else { let resolvedInput = await this._textInput.resolve(); if (!(resolvedInput instanceof BinaryEditorModel)) { - resolvedInput.textEditorModel.onBeforeAttached(); + (resolvedInput as ITextEditorModel).textEditorModel.onBeforeAttached(); } - textOrUntitledEditorModel = resolvedInput as TextFileEditorModel | UntitledTextEditorModel | ResourceEditorModel; + textOrUntitledEditorModel = resolvedInput as TextFileEditorModel | UntitledTextEditorModel | TextResourceEditorModel; } } else { const textEditorModelReference = await this.textModelService.createModelReference(this.resource); textEditorModelReference.object.textEditorModel.onBeforeAttached(); await textEditorModelReference.object.resolve(); - textOrUntitledEditorModel = textEditorModelReference.object as TextFileEditorModel | ResourceEditorModel; + textOrUntitledEditorModel = textEditorModelReference.object as TextFileEditorModel | TextResourceEditorModel; } this._model = this._register(this.instantiationService.createInstance(NotebookEditorModel, this.resource, textOrUntitledEditorModel)); this.hookDirtyListener(this._model.onDidChangeDirty, () => this._onDidChangeDirty.fire()); diff --git a/src/sql/workbench/contrib/notebook/browser/models/notebookInputFactory.ts b/src/sql/workbench/contrib/notebook/browser/models/notebookInputFactory.ts index d9ada47159..518ca1cd81 100644 --- a/src/sql/workbench/contrib/notebook/browser/models/notebookInputFactory.ts +++ b/src/sql/workbench/contrib/notebook/browser/models/notebookInputFactory.ts @@ -9,7 +9,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { FILE_EDITOR_INPUT_ID } from 'vs/workbench/contrib/files/common/files'; import { FileNotebookInput } from 'sql/workbench/contrib/notebook/browser/models/fileNotebookInput'; import { UntitledNotebookInput } from 'sql/workbench/contrib/notebook/browser/models/untitledNotebookInput'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { ILanguageAssociation } from 'sql/workbench/services/languageAssociation/common/languageAssociation'; import { NotebookInput } from 'sql/workbench/contrib/notebook/browser/models/notebookInput'; @@ -44,7 +44,7 @@ export class NotebookEditorInputAssociation implements ILanguageAssociation { return undefined; } - syncConvertinput(activeEditor: IEditorInput): NotebookInput | DiffNotebookInput | undefined { + syncConvertInput(activeEditor: IEditorInput): NotebookInput | DiffNotebookInput | undefined { return this.convertInput(activeEditor); } diff --git a/src/sql/workbench/contrib/notebook/browser/models/untitledNotebookInput.ts b/src/sql/workbench/contrib/notebook/browser/models/untitledNotebookInput.ts index 25af47a618..f1bdb9f105 100644 --- a/src/sql/workbench/contrib/notebook/browser/models/untitledNotebookInput.ts +++ b/src/sql/workbench/contrib/notebook/browser/models/untitledNotebookInput.ts @@ -10,6 +10,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { NotebookInput } from 'sql/workbench/contrib/notebook/browser/models/notebookInput'; import { INotebookService } from 'sql/workbench/services/notebook/browser/notebookService'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; +import { EditorInputCapabilities } from 'vs/workbench/common/editor'; export class UntitledNotebookInput extends NotebookInput { public static ID: string = 'workbench.editorinputs.untitledNotebookInput'; @@ -34,9 +35,9 @@ export class UntitledNotebookInput extends NotebookInput { this.textInput.setMode(mode); } - override isUntitled(): boolean { + override get capabilities(): EditorInputCapabilities { // Subclasses need to explicitly opt-in to being untitled. - return true; + return EditorInputCapabilities.Untitled; } override get typeId(): string { diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts index b5ff1550e3..c79a4cde9b 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts @@ -589,7 +589,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe let secondary: IAction[] = []; let notebookBarMenu = this.menuService.createMenu(MenuId.NotebookToolbar, this.contextKeyService); let groups = notebookBarMenu.getActions({ arg: null, shouldForwardArgs: true }); - fillInActions(groups, { primary, secondary }, false, '', Number.MAX_SAFE_INTEGER, (action: SubmenuAction, group: string, groupSize: number) => group === undefined || group === ''); + fillInActions(groups, { primary, secondary }, false, g => g === '', Number.MAX_SAFE_INTEGER, (action: SubmenuAction, group: string, groupSize: number) => group === undefined || group === ''); this.addPrimaryContributedActions(primary); } diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts index 681ef6c5c6..0295ffa7b9 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -57,7 +57,7 @@ import { INotebookManager } from 'sql/workbench/services/notebook/browser/notebo import { NotebookExplorerViewletViewsContribution } from 'sql/workbench/contrib/notebook/browser/notebookExplorer/notebookExplorerViewlet'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ContributedEditorPriority, IEditorOverrideService } from 'vs/workbench/services/editor/common/editorOverrideService'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { IModeService } from 'vs/editor/common/services/modeService'; import { ILogService } from 'vs/platform/log/common/log'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; @@ -717,7 +717,7 @@ export class NotebookEditorOverrideContribution extends Disposable implements IW // Create the selector from the list of all the language extensions we want to associate with the // notebook editor (filtering out any languages which didn't have any extensions registered yet) const selector = `*{${langExtensions.join(',')}}`; - this._registeredOverrides.add(this._editorOverrideService.registerContributionPoint( + this._registeredOverrides.add(this._editorOverrideService.registerEditor( selector, { id: NotebookEditor.ID, @@ -745,7 +745,7 @@ export class NotebookEditorOverrideContribution extends Disposable implements IW private tryConvertInput(input: IEditorInput, lang: string): IEditorInput | undefined { const langAssociation = languageAssociationRegistry.getAssociationForLanguage(lang); - const notebookEditorInput = langAssociation?.syncConvertinput?.(input); + const notebookEditorInput = langAssociation?.syncConvertInput?.(input); if (!notebookEditorInput) { this._logService.warn('Unable to create input for overriding editor ', input instanceof DiffEditorInput ? `${input.primary.resource.toString()} <-> ${input.secondary.resource.toString()}` : input.resource.toString()); return undefined; diff --git a/src/sql/workbench/contrib/notebook/browser/notebookEditor.component.ts b/src/sql/workbench/contrib/notebook/browser/notebookEditor.component.ts index abfc9b1df2..0af61b2e5a 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookEditor.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookEditor.component.ts @@ -171,7 +171,7 @@ export class NotebookEditorComponent extends AngularDisposable { let secondary: IAction[] = []; let notebookBarMenu = this.menuService.createMenu(MenuId.NotebookToolbar, this.contextKeyService); let groups = notebookBarMenu.getActions({ arg: null, shouldForwardArgs: true }); - fillInActions(groups, { primary, secondary }, false, '', Number.MAX_SAFE_INTEGER, (action: SubmenuAction, group: string, groupSize: number) => group === undefined || group === ''); + fillInActions(groups, { primary, secondary }, false, g => g === '', Number.MAX_SAFE_INTEGER, (action: SubmenuAction, group: string, groupSize: number) => group === undefined || group === ''); } private get modelFactory(): IModelFactory { diff --git a/src/sql/workbench/contrib/notebook/browser/notebookEditor.ts b/src/sql/workbench/contrib/notebook/browser/notebookEditor.ts index 1dc6ddd82c..fee981ca2a 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookEditor.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; import * as DOM from 'vs/base/browser/dom'; import { bootstrapAngular } from 'sql/workbench/services/bootstrap/browser/bootstrapService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -35,6 +35,7 @@ import { NotebookFindDecorations } from 'sql/workbench/contrib/notebook/browser/ import { TimeoutTimer } from 'vs/base/common/async'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; import { onUnexpectedError } from 'vs/base/common/errors'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; export class NotebookEditor extends EditorPane implements IFindNotebookController { @@ -190,7 +191,7 @@ export class NotebookEditor extends EditorPane implements IFindNotebookControlle } } - public override async setInput(input: NotebookInput, options: EditorOptions, context: IEditorOpenContext): Promise { + public override async setInput(input: NotebookInput, options: IEditorOptions, context: IEditorOpenContext): Promise { if (this.input && this.input.matches(input)) { return Promise.resolve(undefined); } diff --git a/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookExplorerViewlet.ts b/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookExplorerViewlet.ts index b94d62aa85..99d980eedb 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookExplorerViewlet.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookExplorerViewlet.ts @@ -6,7 +6,7 @@ import { localize } from 'vs/nls'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IAction } from 'vs/base/common/actions'; -import { $, toggleClass, Dimension, IFocusTracker, getTotalHeight, prepend } from 'vs/base/browser/dom'; +import { $, Dimension, IFocusTracker, getTotalHeight, prepend } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -381,7 +381,7 @@ export class NotebookExplorerViewPaneContainer extends ViewPaneContainer { } override layout(dimension: Dimension): void { - toggleClass(this.root, 'narrow', dimension.width <= 300); + this.root.classList.toggle('narrow', dimension.width <= 300); super.layout(new Dimension(dimension.width, dimension.height - getTotalHeight(this.searchWidgetsContainerElement))); } diff --git a/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearch.ts b/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearch.ts index 1091bb8c9b..ad81ac4b2b 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearch.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearch.ts @@ -86,11 +86,11 @@ export class NotebookSearchView extends SearchView { @IStorageService storageService: IStorageService, @IOpenerService openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService, - @ICommandService readonly commandService: ICommandService, + @ICommandService commandService: ICommandService, @IAdsTelemetryService private _telemetryService: IAdsTelemetryService, ) { - super(options, fileService, editorService, codeEditorService, progressService, notificationService, dialogService, contextViewService, instantiationService, viewDescriptorService, configurationService, contextService, searchWorkbenchService, contextKeyService, replaceService, textFileService, preferencesService, themeService, searchHistoryService, contextMenuService, menuService, accessibilityService, keybindingService, storageService, openerService, telemetryService); + super(options, fileService, editorService, codeEditorService, progressService, notificationService, dialogService, commandService, contextViewService, instantiationService, viewDescriptorService, configurationService, contextService, searchWorkbenchService, contextKeyService, replaceService, textFileService, preferencesService, themeService, searchHistoryService, contextMenuService, menuService, accessibilityService, keybindingService, storageService, openerService, telemetryService); this.memento = new Memento(this.id, storageService); this.viewletState = this.memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); @@ -164,7 +164,7 @@ export class NotebookSearchView extends SearchView { } const actionsPosition = this.searchConfig.actionsPosition; - dom.toggleClass(this.getContainer(), SearchView.ACTIONS_RIGHT_CLASS_NAME, actionsPosition === 'right'); + this.getContainer().classList.toggle(SearchView.ACTIONS_RIGHT_CLASS_NAME, actionsPosition === 'right'); const messagesSize = this.messagesElement.style.display === 'none' ? 0 : diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCodeCell.component.ts b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCodeCell.component.ts index d86f481a68..61eb5e5be2 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCodeCell.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCodeCell.component.ts @@ -85,7 +85,7 @@ export class NotebookViewsCellModel extends CellModel { */ public override get outputs(): Array { return super.outputs - .filter((output: nb.IDisplayResult) => output.data === undefined || output?.data['text/plain'] !== '') + .filter((output: nb.ICellOutput) => (output as nb.IDisplayResult)?.data === undefined || (output as nb.IDisplayResult)?.data['text/plain'] !== '') .map((output: nb.ICellOutput) => ({ ...output })) .map((output: nb.ICellOutput) => { output.metadata = { ...output.metadata }; return output; }); } diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.ts b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.ts index 2e66fb84d8..a4516dc91c 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.ts @@ -8,7 +8,7 @@ import { Component, OnInit, ViewChildren, QueryList, Input, Inject, forwardRef, import { NotebookViewsCardComponent } from 'sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component'; import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel'; -import { GridStack, GridStackEvent, GridStackNode } from 'gridstack'; +import { GridItemHTMLElement, GridStack, GridStackEvent, GridStackNode } from 'gridstack'; import { localize } from 'vs/nls'; import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension'; import { CellChangeEvent, INotebookView, INotebookViewCell } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews'; @@ -72,9 +72,13 @@ export class NotebookViewsGridComponent extends AngularDisposable implements OnI this._loaded = true; this.detectChanges(); - self._grid.on('added', function (e: Event, items: GridStackNode[]) { if (self._gridEnabled) { self.persist('added', items, self._grid, self._items); } }); - self._grid.on('removed', function (e: Event, items: GridStackNode[]) { if (self._gridEnabled) { self.persist('removed', items, self._grid, self._items); } }); - self._grid.on('change', function (e: Event, items: GridStackNode[]) { if (self._gridEnabled) { self.persist('change', items, self._grid, self._items); } }); + let getGridStackItems = (items: GridStackNode[] | GridItemHTMLElement): GridStackNode[] => { + return Array.isArray(items) ? items : (items?.gridstackNode ? [items.gridstackNode] : []); + }; + + self._grid.on('added', function (e: Event, items: GridStackNode[] | GridItemHTMLElement) { if (self._gridEnabled) { self.persist('added', getGridStackItems(items), self._grid, self._items); } }); + self._grid.on('removed', function (e: Event, items: GridStackNode[] | GridItemHTMLElement) { if (self._gridEnabled) { self.persist('removed', getGridStackItems(items), self._grid, self._items); } }); + self._grid.on('change', function (e: Event, items: GridStackNode[] | GridItemHTMLElement) { if (self._gridEnabled) { self.persist('change', getGridStackItems(items), self._grid, self._items); } }); } ngAfterContentChecked() { diff --git a/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts b/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts index 320eac6846..6f66e13c4e 100644 --- a/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts @@ -40,7 +40,6 @@ import { IQueryModelService } from 'sql/workbench/services/query/common/queryMod import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { values } from 'vs/base/common/collections'; import { URI } from 'vs/base/common/uri'; -import { assign } from 'vs/base/common/objects'; import { QueryResultId } from 'sql/workbench/services/notebook/browser/models/cell'; import { equals } from 'vs/base/common/arrays'; import { IDisposableDataProvider } from 'sql/base/common/dataProvider'; @@ -481,7 +480,7 @@ export class DataResourceDataProvider implements IGridDataProvider { return result; }; - let serializeRequestParams: SerializeDataParams = assign(serializer.getBasicSaveParameters(format), >{ + let serializeRequestParams: SerializeDataParams = Object.assign(serializer.getBasicSaveParameters(format), >{ saveFormat: format, columns: columns, filePath: filePath.fsPath, diff --git a/src/sql/workbench/contrib/notebook/test/browser/dataResourceDataProvider.test.ts b/src/sql/workbench/contrib/notebook/test/browser/dataResourceDataProvider.test.ts index a9fc0f3e19..7697069427 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/dataResourceDataProvider.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/dataResourceDataProvider.test.ts @@ -15,7 +15,7 @@ import { DataResourceDataProvider } from '../../browser/outputs/gridOutput.compo import { IDataResource } from 'sql/workbench/services/notebook/browser/sql/sqlSessionManager'; import { ResultSetSummary } from 'sql/workbench/services/query/common/query'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; -import { TestFileDialogService, TestEditorService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestFileDialogService, TestEditorService, TestPathService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; import { SerializationService } from 'sql/platform/serialization/common/serializationService'; import { SaveFormat, ResultSerializer } from 'sql/workbench/services/query/common/resultSerializer'; @@ -47,7 +47,7 @@ export class TestSerializationProvider implements azdata.SerializationProvider { } suite('Data Resource Data Provider', function () { - let fileDialogService: TypeMoq.Mock; + let fileDialogService: TestFileDialogService; let serializer: ResultSerializer; let notificationService: TestNotificationService; let serializationService: SerializationService; @@ -75,7 +75,8 @@ suite('Data Resource Data Provider', function () { let editorService = TypeMoq.Mock.ofType(TestEditorService, TypeMoq.MockBehavior.Strict); editorService.setup(x => x.openEditor(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); let contextService = new TestContextService(); - fileDialogService = TypeMoq.Mock.ofType(TestFileDialogService, TypeMoq.MockBehavior.Strict); + let pathService = new TestPathService(); + fileDialogService = new TestFileDialogService(pathService); notificationService = new TestNotificationService(); serializationService = new SerializationService(undefined, undefined); //_connectionService _capabilitiesService serializationService.registerProvider('testProviderId', new TestSerializationProvider()); @@ -84,7 +85,7 @@ suite('Data Resource Data Provider', function () { undefined, // IConfigurationService editorService.object, contextService, - fileDialogService.object, + fileDialogService, notificationService, undefined // IOpenerService ); @@ -108,15 +109,15 @@ suite('Data Resource Data Provider', function () { instantiationService.object ); let noHeadersFile = URI.file(path.join(tempFolderPath, 'result_noHeaders.csv')); - let fileDialogServiceStub = sinon.stub(fileDialogService.object, 'showSaveDialog').returns(Promise.resolve(noHeadersFile)); - let serializerStub = sinon.stub(serializer, 'getBasicSaveParameters').returns({ resultFormat: SaveFormat.CSV as string, includeHeaders: false }); + let fileDialogServiceStub = sinon.stub(fileDialogService, 'showSaveDialog').returns(Promise.resolve(noHeadersFile)); + let serializerStub = sinon.stub(serializer, 'getBasicSaveParameters').returns({ resultFormat: SaveFormat.CSV as string, includeHeaders: false }); await dataResourceDataProvider.serializeResults(SaveFormat.CSV, undefined); fileDialogServiceStub.restore(); serializerStub.restore(); let withHeadersFile = URI.file(path.join(tempFolderPath, 'result_withHeaders.csv')); - fileDialogServiceStub = sinon.stub(fileDialogService.object, 'showSaveDialog').returns(Promise.resolve(withHeadersFile)); - serializerStub = sinon.stub(serializer, 'getBasicSaveParameters').returns({ resultFormat: SaveFormat.CSV as string, includeHeaders: true }); + fileDialogServiceStub = sinon.stub(fileDialogService, 'showSaveDialog').returns(Promise.resolve(withHeadersFile)); + serializerStub = sinon.stub(serializer, 'getBasicSaveParameters').returns({ resultFormat: SaveFormat.CSV as string, includeHeaders: true }); await dataResourceDataProvider.serializeResults(SaveFormat.CSV, undefined); fileDialogServiceStub.restore(); serializerStub.restore(); diff --git a/src/sql/workbench/contrib/notebook/test/browser/markdownTextTransformer.test.ts b/src/sql/workbench/contrib/notebook/test/browser/markdownTextTransformer.test.ts index 8c6a481abc..cf02de1277 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/markdownTextTransformer.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/markdownTextTransformer.test.ts @@ -73,7 +73,7 @@ suite('MarkdownTextTransformer', () => { undefined, undefined, ); - mockNotebookService = TypeMoq.Mock.ofInstance(notebookService); + mockNotebookService = TypeMoq.Mock.ofInstance(notebookService); cellModel = new CellModel(undefined, undefined, mockNotebookService.object); notebookEditor = new NotebookEditorStub({ cellGuid: cellModel.cellGuid, instantiationService: instantiationService }); diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookActions.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookActions.test.ts index f238b1c42d..eff36fddf4 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookActions.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookActions.test.ts @@ -577,7 +577,7 @@ suite('Notebook Actions', function (): void { let setOptionsSpy: sinon.SinonSpy; setup(async () => { - sandbox = sinon.sandbox.create(); + sandbox = sinon.createSandbox(); container = document.createElement('div'); contextViewProvider = new ContextViewProviderStub(); const instantiationService = workbenchInstantiationService(); diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookEditor.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookEditor.test.ts index d50bb910b8..f403282148 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookEditor.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookEditor.test.ts @@ -44,7 +44,6 @@ import { ILogService } from 'vs/platform/log/common/log'; 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 { EditorOptions } from 'vs/workbench/common/editor'; import { ICell } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -58,6 +57,7 @@ import { workbenchInstantiationService } from 'vs/workbench/test/browser/workben import { IProductService } from 'vs/platform/product/common/productService'; import { IHostColorSchemeService } from 'vs/workbench/services/themes/common/hostColorSchemeService'; import { CellModel } from 'sql/workbench/services/notebook/browser/models/cell'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; class NotebookModelStub extends stubs.NotebookModelStub { public contentChangedEmitter = new Emitter(); @@ -132,7 +132,7 @@ suite('Test class NotebookEditor:', () => { const testNotebookEditor = new NotebookEditorStub({ cellGuid: cellTextEditorGuid, editor: queryTextEditor, model: notebookModel, notebookParams: { notebookUri: untitledNotebookInput.notebookUri } }); notebookService.addNotebookEditor(testNotebookEditor); notebookEditor.clearInput(); - await notebookEditor.setInput(untitledNotebookInput, EditorOptions.create({ pinned: true }), undefined); + await notebookEditor.setInput(untitledNotebookInput, { pinned: true }, undefined); untitledNotebookInput.notebookFindModel.notebookModel = undefined; // clear preexisting notebookModel const result = await notebookEditor.getNotebookModel(); assert.strictEqual(result, notebookModel, `getNotebookModel() should return the model set in the INotebookEditor object`); @@ -215,7 +215,7 @@ suite('Test class NotebookEditor:', () => { test('Tests setInput call with various states of input on a notebookEditor object', async () => { createEditor(notebookEditor); - const editorOptions = EditorOptions.create({ pinned: true }); + const editorOptions: IEditorOptions = { pinned: true }; for (const input of [ untitledNotebookInput /* set to a known input */, untitledNotebookInput /* tries to set the same input that was previously set */ @@ -227,7 +227,7 @@ suite('Test class NotebookEditor:', () => { test('Tests setInput call with various states of findState.isRevealed on a notebookEditor object', async () => { createEditor(notebookEditor); - const editorOptions = EditorOptions.create({ pinned: true }); + const editorOptions: IEditorOptions = { pinned: true }; for (const isRevealed of [true, false]) { notebookEditor['_findState']['_isRevealed'] = isRevealed; notebookEditor.clearInput(); @@ -754,7 +754,7 @@ async function setupNotebookEditor(notebookEditor: NotebookEditor, untitledNoteb } async function setInputDocument(notebookEditor: NotebookEditor, untitledNotebookInput: UntitledNotebookInput): Promise { - const editorOptions = EditorOptions.create({ pinned: true }); + const editorOptions: IEditorOptions = { pinned: true }; await notebookEditor.setInput(untitledNotebookInput, editorOptions, undefined); assert.strictEqual(notebookEditor.options, editorOptions, 'NotebookEditor options must be the ones that we set'); } diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts index 12e9eefbb2..5749359283 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts @@ -20,6 +20,7 @@ import { IExtensionService, NullExtensionService } from 'vs/workbench/services/e import { INotebookService, IProviderInfo } from 'sql/workbench/services/notebook/browser/notebookService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; +import { EditorInputCapabilities } from 'vs/workbench/common/editor'; suite('Notebook Input', function (): void { const instantiationService = workbenchInstantiationService(); @@ -64,13 +65,13 @@ suite('Notebook Input', function (): void { let inputId = fileNotebookInput.typeId; assert.strictEqual(inputId, FileNotebookInput.ID); - assert.strictEqual(fileNotebookInput.isUntitled(), false, 'File Input should not be untitled'); + assert.strictEqual(fileNotebookInput.hasCapability(EditorInputCapabilities.Untitled), false, 'File Input should not be untitled'); }); test('Untitled Notebook Input', async function (): Promise { let inputId = untitledNotebookInput.typeId; assert.strictEqual(inputId, UntitledNotebookInput.ID); - assert.ok(untitledNotebookInput.isUntitled(), 'Untitled Input should be untitled'); + assert.ok(untitledNotebookInput.hasCapability(EditorInputCapabilities.Untitled), 'Untitled Input should be untitled'); }); test('Getters and Setters', async function (): Promise { diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookService.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookService.test.ts index 9a457fd532..dacf0f3567 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookService.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookService.test.ts @@ -164,7 +164,7 @@ suite.skip('NotebookService:', function (): void { notebookService = new NotebookService(lifecycleService, storageService, extensionServiceMock.object, extensionManagementService, instantiationService, fileService, logServiceMock.object, queryManagementService, contextService, productService, editorService, untitledTextEditorService, editorGroupsService, configurationService); - sandbox = sinon.sandbox.create(); + sandbox = sinon.createSandbox(); }); teardown(() => { @@ -412,7 +412,7 @@ suite.skip('NotebookService:', function (): void { id: 'id1' } }); - const targetMethodSpy = sandbox.spy(notebookService, methodName); + const targetMethodSpy = sandbox.spy(notebookService, methodName as keyof NotebookService); didUninstallExtensionEmitter.fire(extensionIdentifier); assert.ok(targetMethodSpy.calledWithExactly(extensionIdentifier.identifier, extensionServiceMock.object), `call arguments to ${methodName} should be ${extensionIdentifier.identifier} & ${extensionServiceMock.object}`); assert.ok(targetMethodSpy.calledOnce, `${methodName} should be called exactly once`); @@ -432,7 +432,7 @@ suite.skip('NotebookService:', function (): void { id: 'id1' } }); - const targetMethodSpy = sandbox.spy(notebookService, methodName); + const targetMethodSpy = sandbox.spy(notebookService, methodName as keyof NotebookService); // the following call will encounter an exception internally with extensionService.getExtensions() returning undefined. didUninstallExtensionEmitter.fire(extensionIdentifier); assert.ok(targetMethodSpy.calledWithExactly(extensionIdentifier.identifier, extensionServiceMock.object), `call arguments to ${methodName} should be ${extensionIdentifier.identifier} & ${extensionServiceMock.object}`); diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts index a8d62630c7..374feec503 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts @@ -240,8 +240,8 @@ suite('NotebookViewModel', function (): void { function setupServices() { mockSessionManager = TypeMoq.Mock.ofType(SessionManager); notebookManagers[0].sessionManager = mockSessionManager.object; - notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose); - capabilitiesService = TypeMoq.Mock.ofType(TestCapabilitiesService); + notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose); + capabilitiesService = TypeMoq.Mock.ofType(TestCapabilitiesService); memento = TypeMoq.Mock.ofType(Memento, TypeMoq.MockBehavior.Loose, ''); memento.setup(x => x.getMemento(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => void 0); queryConnectionService = TypeMoq.Mock.ofType(TestConnectionManagementService, TypeMoq.MockBehavior.Loose, memento.object, undefined, new TestStorageService()); diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookViewsActions.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookViewsActions.test.ts index d4906eb4b8..1607757db7 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookViewsActions.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookViewsActions.test.ts @@ -73,7 +73,7 @@ suite('Notebook Views Actions', function (): void { let sandbox: sinon.SinonSandbox; setup(() => { - sandbox = sinon.sandbox.create(); + sandbox = sinon.createSandbox(); setupServices(); }); @@ -95,7 +95,7 @@ suite('Notebook Views Actions', function (): void { assert.deepStrictEqual(notebookViews.getActiveView(), newView, 'Active view not set properly'); const deleteAction = new DeleteViewAction(notebookViews, dialogService, notificationService); - sandbox.stub(deleteAction, 'confirmDelete').withArgs(newView).returns(Promise.resolve(true)); + sandbox.stub(deleteAction, 'confirmDelete' as keyof DeleteViewAction).withArgs(newView).returns(Promise.resolve(true)); await deleteAction.run(); assert.strictEqual(notebookViews.getViews().length, 0, 'View not deleted'); @@ -116,7 +116,7 @@ suite('Notebook Views Actions', function (): void { assert.strictEqual(notebookViews.getActiveView(), newView, 'Active view not set properly'); const deleteAction = new DeleteViewAction(notebookViews, dialogService, notificationService); - sandbox.stub(deleteAction, 'confirmDelete').withArgs(newView).returns(Promise.resolve(false)); + sandbox.stub(deleteAction, 'confirmDelete' as keyof DeleteViewAction).withArgs(newView).returns(Promise.resolve(false)); await deleteAction.run(); assert.strictEqual(notebookViews.getViews().length, 1, 'View should not have deleted'); @@ -165,8 +165,8 @@ suite('Notebook Views Actions', function (): void { function setupServices() { mockSessionManager = TypeMoq.Mock.ofType(SessionManager); notebookManagers[0].sessionManager = mockSessionManager.object; - notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose); - capabilitiesService = TypeMoq.Mock.ofType(TestCapabilitiesService); + notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose); + capabilitiesService = TypeMoq.Mock.ofType(TestCapabilitiesService); memento = TypeMoq.Mock.ofType(Memento, TypeMoq.MockBehavior.Loose, ''); memento.setup(x => x.getMemento(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => void 0); queryConnectionService = TypeMoq.Mock.ofType(TestConnectionManagementService, TypeMoq.MockBehavior.Loose, memento.object, undefined, new TestStorageService()); diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookViewsExtension.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookViewsExtension.test.ts index 97020ade15..509f2bcf41 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookViewsExtension.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookViewsExtension.test.ts @@ -132,8 +132,8 @@ suite('NotebookViews', function (): void { function setupServices() { mockSessionManager = TypeMoq.Mock.ofType(SessionManager); notebookManagers[0].sessionManager = mockSessionManager.object; - notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose); - capabilitiesService = TypeMoq.Mock.ofType(TestCapabilitiesService); + notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose); + capabilitiesService = TypeMoq.Mock.ofType(TestCapabilitiesService); memento = TypeMoq.Mock.ofType(Memento, TypeMoq.MockBehavior.Loose, ''); memento.setup(x => x.getMemento(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => void 0); queryConnectionService = TypeMoq.Mock.ofType(TestConnectionManagementService, TypeMoq.MockBehavior.Loose, memento.object, undefined, new TestStorageService()); diff --git a/src/sql/workbench/contrib/notebook/test/electron-browser/cell.test.ts b/src/sql/workbench/contrib/notebook/test/electron-browser/cell.test.ts index 3d39fcede0..1a72701635 100644 --- a/src/sql/workbench/contrib/notebook/test/electron-browser/cell.test.ts +++ b/src/sql/workbench/contrib/notebook/test/electron-browser/cell.test.ts @@ -19,7 +19,6 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { isUndefinedOrNull } from 'vs/base/common/types'; -import { startsWith } from 'vs/base/common/strings'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; @@ -741,15 +740,15 @@ suite('Cell Model', function (): void { let content = JSON.stringify(cell.toJSON(), undefined, ' '); let contentSplit = content.split('\n'); assert.equal(contentSplit.length, 9); - assert(startsWith(contentSplit[0].trim(), '{')); - assert(startsWith(contentSplit[1].trim(), '"cell_type": "code",')); - assert(startsWith(contentSplit[2].trim(), '"source": ""')); - assert(startsWith(contentSplit[3].trim(), '"metadata": {')); - assert(startsWith(contentSplit[4].trim(), '"azdata_cell_guid": "')); - assert(startsWith(contentSplit[5].trim(), '}')); - assert(startsWith(contentSplit[6].trim(), '"outputs": []')); - assert(startsWith(contentSplit[7].trim(), '"execution_count": null')); - assert(startsWith(contentSplit[8].trim(), '}')); + assert(contentSplit[0].trim().startsWith('{')); + assert(contentSplit[1].trim().startsWith('"cell_type": "code",')); + assert(contentSplit[2].trim().startsWith('"source": ""')); + assert(contentSplit[3].trim().startsWith('"metadata": {')); + assert(contentSplit[4].trim().startsWith('"azdata_cell_guid": "')); + assert(contentSplit[5].trim().startsWith('}')); + assert(contentSplit[6].trim().startsWith('"outputs": []')); + assert(contentSplit[7].trim().startsWith('"execution_count": null')); + assert(contentSplit[8].trim().startsWith('}')); }); }); diff --git a/src/sql/workbench/contrib/notebook/test/electron-browser/clientSession.test.ts b/src/sql/workbench/contrib/notebook/test/electron-browser/clientSession.test.ts index d95f795b3b..cdd18c6034 100644 --- a/src/sql/workbench/contrib/notebook/test/electron-browser/clientSession.test.ts +++ b/src/sql/workbench/contrib/notebook/test/electron-browser/clientSession.test.ts @@ -30,7 +30,7 @@ suite('Client Session', function (): void { notebookManager = new NotebookManagerStub(); notebookManager.serverManager = serverManager; notebookManager.sessionManager = mockSessionManager.object; - notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose); + notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose); session = new ClientSession({ notebookManager: notebookManager, 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 44b3bc62ea..27d91b8371 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 @@ -58,11 +58,11 @@ suite('Local Content Manager', function (): void { override async readFile(resource: URI, options?: IReadFileOptions | undefined): Promise { const content = await promisify(fs.readFile)(resource.fsPath); - return { name: ',', size: 0, etag: '', mtime: 0, value: VSBuffer.fromString(content.toString()), resource, ctime: 0 }; + return { name: ',', size: 0, etag: '', mtime: 0, value: VSBuffer.fromString(content.toString()), resource, ctime: 0, readonly: false }; } override async writeFile(resource: URI, bufferOrReadable: VSBuffer | VSBufferReadable, options?: IWriteFileOptions): Promise { - await pfs.writeFile(resource.fsPath, bufferOrReadable.toString()); - return { resource: resource, mtime: 0, etag: '', size: 0, name: '', isDirectory: false, ctime: 0, isFile: true, isSymbolicLink: false }; + await pfs.Promises.writeFile(resource.fsPath, bufferOrReadable.toString()); + return { resource: resource, mtime: 0, etag: '', size: 0, name: '', isDirectory: false, ctime: 0, isFile: true, isSymbolicLink: false, readonly: false }; } }; instantiationService.set(IFileService, fileService); 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 c5cfa72836..5c28639f7c 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 @@ -34,8 +34,6 @@ import { nb } from 'azdata'; import { Emitter } from 'vs/base/common/event'; import { INotebookEditor, INotebookManager } from 'sql/workbench/services/notebook/browser/notebookService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { startsWith } from 'vs/base/common/strings'; -import { assign } from 'vs/base/common/objects'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { TestStorageService, TestTextResourcePropertiesService } from 'vs/workbench/test/common/workbenchTestServices'; @@ -172,7 +170,7 @@ suite('Notebook Editor Model', function (): void { teardown(() => { if (accessor && accessor.textFileService && accessor.textFileService.files) { - (accessor.textFileService.files).clear(); + (accessor.textFileService.files).dispose(); } }); @@ -663,7 +661,7 @@ suite('Notebook Editor Model', function (): void { assert.equal(notebookEditorModel.editorModel.textEditorModel.getLineContent(10 + i * 21), ' ],'); assert.equal(notebookEditorModel.editorModel.textEditorModel.getLineContent(14 + i * 21), ' "outputs": ['); assert.equal(notebookEditorModel.editorModel.textEditorModel.getLineContent(25 + i * 21), ' "execution_count": null'); - assert(startsWith(notebookEditorModel.editorModel.textEditorModel.getLineContent(26 + i * 21), ' }')); + assert(notebookEditorModel.editorModel.textEditorModel.getLineContent(26 + i * 21).startsWith(' }')); } }); @@ -971,7 +969,7 @@ suite('Notebook Editor Model', function (): void { }); async function createNewNotebookModel() { - let options: INotebookModelOptions = assign({}, defaultModelOptions, >{ + let options: INotebookModelOptions = Object.assign({}, defaultModelOptions, >{ factory: mockModelFactory.object }); notebookModel = new NotebookModel(options, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService); diff --git a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookFindModel.test.ts b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookFindModel.test.ts index a3c31d3a4c..aad6600569 100644 --- a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookFindModel.test.ts +++ b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookFindModel.test.ts @@ -79,8 +79,8 @@ suite('Notebook Find Model', function (): void { setup(async () => { sessionReady = new Deferred(); - notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose); - capabilitiesService = TypeMoq.Mock.ofType(TestCapabilitiesService); + notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose); + capabilitiesService = TypeMoq.Mock.ofType(TestCapabilitiesService); memento = TypeMoq.Mock.ofType(Memento, TypeMoq.MockBehavior.Loose, ''); memento.setup(x => x.getMemento(TypeMoq.It.isAny(), TypeMoq.It.isAny() )).returns(() => void 0); @@ -102,7 +102,7 @@ suite('Notebook Find Model', function (): void { layoutChanged: undefined, capabilitiesService: capabilitiesService.object }; - mockClientSession = TypeMoq.Mock.ofType(ClientSession, undefined, defaultModelOptions); + mockClientSession = TypeMoq.Mock.ofType(ClientSession, undefined, defaultModelOptions); mockClientSession.setup(c => c.initialize()).returns(() => { return Promise.resolve(); }); diff --git a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookModel.test.ts b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookModel.test.ts index 78f5bc6695..fc436b1887 100644 --- a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookModel.test.ts +++ b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookModel.test.ts @@ -29,7 +29,6 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { NullLogService } from 'vs/platform/log/common/log'; import { TestConnectionManagementService } from 'sql/platform/connection/test/common/testConnectionManagementService'; import { isUndefinedOrNull } from 'vs/base/common/types'; -import { assign } from 'vs/base/common/objects'; import { NotebookEditorContentManager } from 'sql/workbench/contrib/notebook/browser/models/notebookInput'; import { SessionManager } from 'sql/workbench/contrib/notebook/test/emptySessionClasses'; import { mssqlProviderName } from 'sql/platform/connection/common/constants'; @@ -38,7 +37,6 @@ import { uriPrefixes } from 'sql/platform/connection/common/utils'; import { NullAdsTelemetryService } from 'sql/platform/telemetry/common/adsTelemetryService'; import { TestConfigurationService } from 'sql/platform/connection/test/common/testConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; let expectedNotebookContent: nb.INotebookContents = { cells: [{ @@ -150,7 +148,7 @@ suite('notebook model', function (): void { mockSessionManager = TypeMoq.Mock.ofType(SessionManager); notebookManagers[0].sessionManager = mockSessionManager.object; sessionReady = new Deferred(); - notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose); + notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose); capabilitiesService = new TestCapabilitiesService(); memento = TypeMoq.Mock.ofType(Memento, TypeMoq.MockBehavior.Loose, ''); memento.setup(x => x.getMemento(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => void 0); @@ -747,13 +745,13 @@ suite('notebook model', function (): void { assert(!isUndefinedOrNull(model.context), 'context should exist after call to change context'); let notebookKernelAlias = model.context.serverCapabilities.notebookKernelAlias; - let doChangeKernelStub = sinon.spy(model, 'doChangeKernel').withArgs(model.kernelAliases[0]); + let doChangeKernelStub = sinon.spy(model, 'doChangeKernel' as keyof NotebookModel); model.changeKernel(notebookKernelAlias); assert.equal(model.selectedKernelDisplayName, notebookKernelAlias); assert.equal(model.currentKernelAlias, notebookKernelAlias); - sinon.assert.called(doChangeKernelStub); - sinon.restore(doChangeKernelStub); + sinon.assert.calledWith(doChangeKernelStub, model.kernelAliases[0]); + doChangeKernelStub.restore(); // After closing the notebook await model.handleClosed(); @@ -774,14 +772,14 @@ suite('notebook model', function (): void { assert(!isUndefinedOrNull(model.context), 'context should exist after call to change context'); let notebookKernelAlias = model.context.serverCapabilities.notebookKernelAlias; - let doChangeKernelStub = sinon.spy(model, 'doChangeKernel'); + let doChangeKernelStub = sinon.spy(model, 'doChangeKernel' as keyof NotebookModel); // Change kernel first to alias kernel and then connect to SQL connection model.changeKernel(notebookKernelAlias); assert.equal(model.selectedKernelDisplayName, notebookKernelAlias); assert.equal(model.currentKernelAlias, notebookKernelAlias); sinon.assert.called(doChangeKernelStub); - sinon.restore(doChangeKernelStub); + doChangeKernelStub.restore(); // Change to SQL connection from Fake connection await changeContextWithConnectionProfile(model); @@ -790,7 +788,7 @@ suite('notebook model', function (): void { assert.equal(model.selectedKernelDisplayName, expectedKernel); assert.equal(model.currentKernelAlias, undefined); sinon.assert.called(doChangeKernelStub); - sinon.restore(doChangeKernelStub); + doChangeKernelStub.restore(); // After closing the notebook await model.handleClosed(); @@ -815,7 +813,7 @@ suite('notebook model', function (): void { defaultModelOptions.contentManager = mockContentManager.object; // And a matching connection profile - let expectedConnectionProfile: IConnectionProfile = { + let expectedConnectionProfile = { connectionName: connectionName, serverName: '', databaseName: '', @@ -915,7 +913,7 @@ suite('notebook model', function (): void { sessionReady.resolve(); let actualSession: IClientSession = undefined; - let options: INotebookModelOptions = assign({}, defaultModelOptions, >{ + let options: INotebookModelOptions = Object.assign({}, defaultModelOptions, >{ factory: mockModelFactory.object }); let model = new NotebookModel(options, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService, capabilitiesService); diff --git a/src/sql/workbench/contrib/objectExplorer/test/browser/serverTreeView.test.ts b/src/sql/workbench/contrib/objectExplorer/test/browser/serverTreeView.test.ts index 212a83d2b3..79f820d8dd 100644 --- a/src/sql/workbench/contrib/objectExplorer/test/browser/serverTreeView.test.ts +++ b/src/sql/workbench/contrib/objectExplorer/test/browser/serverTreeView.test.ts @@ -39,7 +39,7 @@ suite('ServerTreeView onAddConnectionProfile handler tests', () => { mockConnectionManagementService.setup(x => x.getConnectionGroups()).returns(x => []); mockConnectionManagementService.setup(x => x.hasRegisteredServers()).returns(() => true); serverTreeView = new ServerTreeView(mockConnectionManagementService.object, instantiationService, undefined, new TestThemeService(), undefined, undefined, capabilitiesService, undefined, undefined, new MockContextKeyService()); - mockTree = TypeMoq.Mock.ofType(TestTree); + mockTree = TypeMoq.Mock.ofType(TestTree); (serverTreeView as any)._tree = mockTree.object; mockRefreshTreeMethod = TypeMoq.Mock.ofType(Function); mockRefreshTreeMethod.setup(x => x()).returns(() => Promise.resolve()); diff --git a/src/sql/workbench/contrib/profiler/browser/profilerEditor.ts b/src/sql/workbench/contrib/profiler/browser/profilerEditor.ts index 1410d85dce..47c12393d3 100644 --- a/src/sql/workbench/contrib/profiler/browser/profilerEditor.ts +++ b/src/sql/workbench/contrib/profiler/browser/profilerEditor.ts @@ -24,7 +24,7 @@ import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import * as nls from 'vs/nls'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { Command } from 'vs/editor/browser/editorExtensions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; @@ -39,7 +39,6 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { IView, SplitView, Sizing } from 'vs/base/browser/ui/splitview/splitview'; import * as DOM from 'vs/base/browser/dom'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { EditorOptions } from 'vs/workbench/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkbenchThemeService, VS_DARK_THEME, VS_HC_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -53,6 +52,8 @@ import { ITextResourcePropertiesService } from 'vs/editor/common/services/textRe import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { attachTabbedPanelStyler } from 'sql/workbench/common/styler'; import { UntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; class BasicView implements IView { public get element(): HTMLElement { @@ -155,6 +156,7 @@ export class ProfilerEditor extends EditorPane { private _savedTableViewStates = new Map(); + private readonly _disposables = new DisposableStore(); constructor( @ITelemetryService telemetryService: ITelemetryService, @IWorkbenchThemeService themeService: IWorkbenchThemeService, @@ -166,23 +168,34 @@ export class ProfilerEditor extends EditorPane { @IEditorService editorService: IEditorService, @IStorageService storageService: IStorageService, @IClipboardService private _clipboardService: IClipboardService, - @ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService + @ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService, + @IEditorGroupsService editorGroupsService: IEditorGroupsService ) { super(ProfilerEditor.ID, telemetryService, themeService, storageService); this._profilerEditorContextKey = CONTEXT_PROFILER_EDITOR.bindTo(this._contextKeyService); - if (editorService) { - editorService.overrideOpenEditor({ - open: (editor, options, group) => { - if (this.isVisible() && (editor !== this.input || group !== this.group)) { - this.saveEditorViewState(); - } - return {}; - } - }); + if (editorGroupsService) { + // Add all the initial groups to be listened to + editorGroupsService.whenReady.then(() => editorGroupsService.groups.forEach(group => { + this.registerGroupListener(group); + })); + + // Additional groups added should also be listened to + this._register(editorGroupsService.onDidAddGroup((group) => this.registerGroupListener(group))); + + this._register(this._disposables); } } + private registerGroupListener(group: IEditorGroup): void { + const listener = group.onWillOpenEditor(e => { + if (this.isVisible() && (e.editor !== this.input || group !== this.group)) { + this.saveEditorViewState(); + } + }); + this._disposables.add(listener); + } + protected createEditor(parent: HTMLElement): void { this._container = document.createElement('div'); this._container.className = 'carbon-profiler'; @@ -307,7 +320,7 @@ export class ProfilerEditor extends EditorPane { profilerTableContainer.classList.add(VS_HC_THEME); } this.themeService.onDidColorThemeChange(e => { - DOM.removeClasses(profilerTableContainer, VS_DARK_THEME, VS_HC_THEME); + profilerTableContainer.classList.remove(VS_DARK_THEME, VS_HC_THEME); if (e.type === ColorScheme.DARK) { profilerTableContainer.classList.add(VS_DARK_THEME); } else if (e.type === ColorScheme.HIGH_CONTRAST) { @@ -447,7 +460,7 @@ export class ProfilerEditor extends EditorPane { return this._input as ProfilerInput; } - public override setInput(input: ProfilerInput, options?: EditorOptions): Promise { + public override setInput(input: ProfilerInput, options?: IEditorOptions): Promise { let savedViewState = this._savedTableViewStates.get(input); this._profilerEditorContextKey.set(true); diff --git a/src/sql/workbench/contrib/profiler/browser/profilerFindWidget.ts b/src/sql/workbench/contrib/profiler/browser/profilerFindWidget.ts index bce56a8562..fec3eba468 100644 --- a/src/sql/workbench/contrib/profiler/browser/profilerFindWidget.ts +++ b/src/sql/workbench/contrib/profiler/browser/profilerFindWidget.ts @@ -127,9 +127,9 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL if (FIND_WIDGET_INITIAL_WIDTH + 28 - MAX_MATCHES_COUNT_WIDTH >= editorWidth + 50) { collapsedFindWidget = true; } - dom.toggleClass(this._domNode, 'collapsed-find-widget', collapsedFindWidget); - dom.toggleClass(this._domNode, 'narrow-find-widget', narrowFindWidget); - dom.toggleClass(this._domNode, 'reduced-find-widget', reducedFindWidget); + this._domNode.classList.toggle('collapsed-find-widget', collapsedFindWidget); + this._domNode.classList.toggle('narrow-find-widget', narrowFindWidget); + this._domNode.classList.toggle('reduced-find-widget', reducedFindWidget); if (!narrowFindWidget && !collapsedFindWidget) { // the minimal left offset of findwidget is 15px. @@ -204,7 +204,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL } if (e.searchString || e.matchesCount || e.matchesPosition) { let showRedOutline = (this._state.searchString.length > 0 && this._state.matchesCount === 0); - dom.toggleClass(this._domNode, 'no-results', showRedOutline); + this._domNode.classList.toggle('no-results', showRedOutline); this._updateMatchesCount(); } @@ -552,7 +552,7 @@ class SimpleButton extends Widget { } public setEnabled(enabled: boolean): void { - dom.toggleClass(this._domNode, 'disabled', !enabled); + this._domNode.classList.toggle('disabled', !enabled); this._domNode.setAttribute('aria-disabled', String(!enabled)); this._domNode.tabIndex = enabled ? 0 : -1; } @@ -562,6 +562,6 @@ class SimpleButton extends Widget { } public toggleClass(className: string, shouldHaveIt: boolean): void { - dom.toggleClass(this._domNode, className, shouldHaveIt); + this._domNode.classList.toggle(className, shouldHaveIt); } } diff --git a/src/sql/workbench/contrib/profiler/browser/profilerResourceEditor.ts b/src/sql/workbench/contrib/profiler/browser/profilerResourceEditor.ts index ac8c322642..dea72bf0bb 100644 --- a/src/sql/workbench/contrib/profiler/browser/profilerResourceEditor.ts +++ b/src/sql/workbench/contrib/profiler/browser/profilerResourceEditor.ts @@ -6,7 +6,7 @@ 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'; +import { TextResourceEditorModel } from 'vs/workbench/common/editor/textResourceEditorModel'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; @@ -15,12 +15,13 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; -import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; class ProfilerResourceCodeEditor extends CodeEditorWidget { @@ -75,9 +76,9 @@ export class ProfilerResourceEditor extends BaseTextEditor { return options; } - override async setInput(input: UntitledTextEditorInput, options: EditorOptions, context: IEditorOpenContext): Promise { + override async setInput(input: UntitledTextEditorInput, options: ITextEditorOptions, context: IEditorOpenContext): Promise { await super.setInput(input, options, context, CancellationToken.None); - const editorModel = await this.input.resolve() as ResourceEditorModel; + const editorModel = await this.input.resolve() as TextResourceEditorModel; await editorModel.resolve(); this.getControl().setModel(editorModel.textEditorModel); } diff --git a/src/sql/workbench/contrib/profiler/browser/profilerTableEditor.ts b/src/sql/workbench/contrib/profiler/browser/profilerTableEditor.ts index 07a6a309bc..edcce1e872 100644 --- a/src/sql/workbench/contrib/profiler/browser/profilerTableEditor.ts +++ b/src/sql/workbench/contrib/profiler/browser/profilerTableEditor.ts @@ -288,7 +288,7 @@ export class ProfilerTableEditor extends EditorPane implements IProfilerControll : localize('ProfilerTableEditor.eventCount', "Events: {0}", this._input.data.getLength()); this._disposeStatusbarItem(); - this._statusbarItem = this._statusbarService.addEntry({ text: message, ariaLabel: message }, 'status.eventCount', localize('status.eventCount', "Event Count"), StatusbarAlignment.RIGHT); + this._statusbarItem = this._statusbarService.addEntry({ name: localize('status.eventCount', "Event Count"), text: message, ariaLabel: message }, 'status.eventCount', StatusbarAlignment.RIGHT); } } diff --git a/src/sql/workbench/contrib/query/common/fileQueryEditorInput.ts b/src/sql/workbench/contrib/query/browser/fileQueryEditorInput.ts similarity index 96% rename from src/sql/workbench/contrib/query/common/fileQueryEditorInput.ts rename to src/sql/workbench/contrib/query/browser/fileQueryEditorInput.ts index 1fe3f9f4c7..5557ab282b 100644 --- a/src/sql/workbench/contrib/query/common/fileQueryEditorInput.ts +++ b/src/sql/workbench/contrib/query/browser/fileQueryEditorInput.ts @@ -8,7 +8,7 @@ import { QueryResultsInput } from 'sql/workbench/common/editor/query/queryResult import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; import { IQueryModelService } from 'sql/workbench/services/query/common/queryModel'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IMoveResult, GroupIdentifier } from 'vs/workbench/common/editor'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; diff --git a/src/sql/workbench/contrib/query/browser/flavorStatus.ts b/src/sql/workbench/contrib/query/browser/flavorStatus.ts index 0527947f70..2e9c8a9647 100644 --- a/src/sql/workbench/contrib/query/browser/flavorStatus.ts +++ b/src/sql/workbench/contrib/query/browser/flavorStatus.ts @@ -62,6 +62,7 @@ export class SqlFlavorStatusbarItem extends Disposable implements IWorkbenchCont private statusItem: IStatusbarEntryAccessor; private _sqlStatusEditors: { [editorUri: string]: SqlProviderEntry }; + private readonly name = nls.localize('status.query.flavor', "SQL Language Flavor"); constructor( @IStatusbarService private readonly statusbarService: IStatusbarService, @@ -73,12 +74,12 @@ export class SqlFlavorStatusbarItem extends Disposable implements IWorkbenchCont this.statusItem = this._register( this.statusbarService.addEntry({ + name: this.name, text: nls.localize('changeProvider', "Change SQL language provider"), ariaLabel: nls.localize('changeProvider', "Change SQL language provider"), command: 'sql.action.editor.changeProvider' }, SqlFlavorStatusbarItem.ID, - nls.localize('status.query.flavor', "SQL Language Flavor"), StatusbarAlignment.RIGHT, 100) ); @@ -160,7 +161,8 @@ export class SqlFlavorStatusbarItem extends Disposable implements IWorkbenchCont private updateFlavorElement(text: string): void { const props: IStatusbarEntry = { - text, + name: this.name, + text: text, ariaLabel: text, command: 'sql.action.editor.changeProvider' }; diff --git a/src/sql/workbench/contrib/query/browser/messagePanel.ts b/src/sql/workbench/contrib/query/browser/messagePanel.ts index a8a2c7c07a..c43861999e 100644 --- a/src/sql/workbench/contrib/query/browser/messagePanel.ts +++ b/src/sql/workbench/contrib/query/browser/messagePanel.ts @@ -15,7 +15,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { WorkbenchDataTree } from 'vs/platform/list/browser/listService'; import { isArray, isString } from 'vs/base/common/types'; import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; -import { $, Dimension, createStyleSheet, addStandardDisposableGenericMouseDownListner, toggleClass } from 'vs/base/browser/dom'; +import { $, Dimension, createStyleSheet, addStandardDisposableGenericMouseDownListner } from 'vs/base/browser/dom'; import { resultsErrorColor } from 'sql/platform/theme/common/colors'; import { CachedListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; import { FuzzyScore } from 'vs/base/common/filters'; @@ -34,7 +34,6 @@ import { IDataTreeViewState } from 'vs/base/browser/ui/tree/dataTree'; import { IRange } from 'vs/editor/common/core/range'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IQueryEditorConfiguration } from 'sql/platform/query/common/query'; -import { push } from 'vs/base/common/arrays'; export interface IResultMessageIntern { id?: string; @@ -109,7 +108,7 @@ export class MessagePanel extends Disposable { ) { super(); const wordWrap = this.configurationService.getValue('queryEditor').messages.wordwrap; - toggleClass(this.container, 'word-wrap', wordWrap); + this.container.classList.toggle('word-wrap', wordWrap); this.tree = >instantiationService.createInstance( WorkbenchDataTree, 'MessagePanel', @@ -202,7 +201,7 @@ export class MessagePanel extends Disposable { private onMessage(message: IQueryMessage | IQueryMessage[], setInput: boolean = false) { if (isArray(message)) { - push(this.model.messages, message); + this.model.messages.push(...message); } else { this.model.messages.push(message); } diff --git a/src/sql/workbench/contrib/query/browser/query.contribution.ts b/src/sql/workbench/contrib/query/browser/query.contribution.ts index 6747fb4b1f..d309a036e3 100644 --- a/src/sql/workbench/contrib/query/browser/query.contribution.ts +++ b/src/sql/workbench/contrib/query/browser/query.contribution.ts @@ -30,7 +30,7 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { TimeElapsedStatusBarContributions, RowCountStatusBarContributions, QueryStatusStatusBarContributions, QueryResultSelectionSummaryStatusBarContribution } from 'sql/workbench/contrib/query/browser/statusBarItems'; import { SqlFlavorStatusbarItem, ChangeFlavorAction } from 'sql/workbench/contrib/query/browser/flavorStatus'; import { EditorExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; -import { FileQueryEditorInput } from 'sql/workbench/contrib/query/common/fileQueryEditorInput'; +import { FileQueryEditorInput } from 'sql/workbench/contrib/query/browser/fileQueryEditorInput'; import { FileQueryEditorInputSerializer, QueryEditorLanguageAssociation, UntitledQueryEditorInputSerializer } from 'sql/workbench/contrib/query/browser/queryInputFactory'; import { UntitledQueryEditorInput } from 'sql/workbench/common/editor/query/untitledQueryEditorInput'; import { ILanguageAssociationRegistry, Extensions as LanguageAssociationExtensions } from 'sql/workbench/services/languageAssociation/common/languageAssociation'; @@ -43,7 +43,7 @@ import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IModeService } from 'vs/editor/common/services/modeService'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { IEditorOverrideService, ContributedEditorPriority } from 'vs/workbench/services/editor/common/editorOverrideService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ILogService } from 'vs/platform/log/common/log'; @@ -523,7 +523,7 @@ export class QueryEditorOverrideContribution extends Disposable implements IWork // Create the selector from the list of all the language extensions we want to associate with the // query editor (filtering out any languages which didn't have any extensions registered yet) const selector = `*{${langExtensions.join(',')}}`; - this._registeredOverrides.add(this._editorOverrideService.registerContributionPoint( + this._registeredOverrides.add(this._editorOverrideService.registerEditor( selector, { id: QueryEditor.ID, @@ -537,7 +537,7 @@ export class QueryEditorOverrideContribution extends Disposable implements IWork resource: resource }) as FileEditorInput; const langAssociation = languageAssociationRegistry.getAssociationForLanguage(lang); - const queryEditorInput = langAssociation?.syncConvertinput?.(fileInput); + const queryEditorInput = langAssociation?.syncConvertInput?.(fileInput); if (!queryEditorInput) { this._logService.warn('Unable to create input for overriding editor ', resource); return undefined; diff --git a/src/sql/workbench/contrib/query/browser/queryEditor.ts b/src/sql/workbench/contrib/query/browser/queryEditor.ts index 7dc9526aa6..1074a892b1 100644 --- a/src/sql/workbench/contrib/query/browser/queryEditor.ts +++ b/src/sql/workbench/contrib/query/browser/queryEditor.ts @@ -8,7 +8,7 @@ import 'vs/css!./media/queryEditor'; import { localize } from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import * as path from 'vs/base/common/path'; -import { EditorOptions, IEditorControl, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorControl, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorPane, EditorMemento } from 'vs/workbench/browser/parts/editor/editorPane'; import { Orientation } from 'vs/base/browser/ui/sash/sash'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -28,7 +28,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { IAction } from 'vs/base/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { URI } from 'vs/base/common/uri'; import { IFileService, FileChangesEvent } from 'vs/platform/files/common/files'; import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; @@ -41,6 +41,7 @@ import * as actions from 'sql/workbench/contrib/query/browser/queryActions'; import { IRange } from 'vs/editor/common/core/range'; import { UntitledQueryEditorInput } from 'sql/workbench/common/editor/query/untitledQueryEditorInput'; import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; const QUERY_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'queryEditorViewState'; @@ -337,7 +338,7 @@ export class QueryEditor extends EditorPane { this.taskbar.setContent(content); } - public override async setInput(newInput: QueryEditorInput, options: EditorOptions, context: IEditorOpenContext, token: CancellationToken): Promise { + public override async setInput(newInput: QueryEditorInput, options: IEditorOptions, context: IEditorOpenContext, token: CancellationToken): Promise { const oldInput = this.input; if (newInput.matches(oldInput)) { @@ -513,7 +514,7 @@ export class QueryEditor extends EditorPane { return this.currentTextEditor.getControl(); } - public override setOptions(options: EditorOptions): void { + public override setOptions(options: IEditorOptions): void { this.currentTextEditor.setOptions(options); } diff --git a/src/sql/workbench/contrib/query/browser/queryInputFactory.ts b/src/sql/workbench/contrib/query/browser/queryInputFactory.ts index d66f5224d7..9817ffe960 100644 --- a/src/sql/workbench/contrib/query/browser/queryInputFactory.ts +++ b/src/sql/workbench/contrib/query/browser/queryInputFactory.ts @@ -9,8 +9,8 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { QueryResultsInput } from 'sql/workbench/common/editor/query/queryResultsInput'; import { FILE_EDITOR_INPUT_ID } from 'vs/workbench/contrib/files/common/files'; import { UntitledQueryEditorInput } from 'sql/workbench/common/editor/query/untitledQueryEditorInput'; -import { FileQueryEditorInput } from 'sql/workbench/contrib/query/common/fileQueryEditorInput'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { FileQueryEditorInput } from 'sql/workbench/contrib/query/browser/fileQueryEditorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { ILanguageAssociation } from 'sql/workbench/services/languageAssociation/common/languageAssociation'; import { QueryEditorInput } from 'sql/workbench/common/editor/query/queryEditorInput'; @@ -71,7 +71,7 @@ export class QueryEditorLanguageAssociation implements ILanguageAssociation { return queryEditorInput; } - syncConvertinput(activeEditor: IEditorInput): QueryEditorInput | undefined { + syncConvertInput(activeEditor: IEditorInput): QueryEditorInput | undefined { const queryResultsInput = this.instantiationService.createInstance(QueryResultsInput, activeEditor.resource.toString(true)); let queryEditorInput: QueryEditorInput; if (activeEditor instanceof FileEditorInput) { diff --git a/src/sql/workbench/contrib/query/browser/queryResultsEditor.ts b/src/sql/workbench/contrib/query/browser/queryResultsEditor.ts index b7b6de400b..369573dbda 100644 --- a/src/sql/workbench/contrib/query/browser/queryResultsEditor.ts +++ b/src/sql/workbench/contrib/query/browser/queryResultsEditor.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; @@ -20,6 +20,7 @@ import { QueryResultsView } from 'sql/workbench/contrib/query/browser/queryResul import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { RESULTS_GRID_DEFAULTS } from 'sql/workbench/common/constants'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; export const TextCompareEditorVisible = new RawContextKey('textCompareEditorVisible', false); @@ -130,7 +131,7 @@ export class QueryResultsEditor extends EditorPane { this.resultsView.layout(dimension); } - override setInput(input: QueryResultsInput, options: EditorOptions, context: IEditorOpenContext): Promise { + override setInput(input: QueryResultsInput, options: IEditorOptions, context: IEditorOpenContext): Promise { super.setInput(input, options, context, CancellationToken.None); this.resultsView.input = input; return Promise.resolve(null); diff --git a/src/sql/workbench/contrib/query/browser/statusBarItems.ts b/src/sql/workbench/contrib/query/browser/statusBarItems.ts index b26846c8e8..1f583adf6b 100644 --- a/src/sql/workbench/contrib/query/browser/statusBarItems.ts +++ b/src/sql/workbench/contrib/query/browser/statusBarItems.ts @@ -24,6 +24,7 @@ export class TimeElapsedStatusBarContributions extends Disposable implements IWo private intervalTimer = new IntervalTimer(); private disposable = this._register(new DisposableStore()); + private readonly name = localize('status.query.timeElapsed', "Time Elapsed"); constructor( @IStatusbarService private readonly statusbarService: IStatusbarService, @@ -33,11 +34,11 @@ export class TimeElapsedStatusBarContributions extends Disposable implements IWo super(); this.statusItem = this._register( this.statusbarService.addEntry({ + name: this.name, text: '', ariaLabel: '' }, TimeElapsedStatusBarContributions.ID, - localize('status.query.timeElapsed', "Time Elapsed"), StatusbarAlignment.RIGHT, 100) ); @@ -93,6 +94,7 @@ export class TimeElapsedStatusBarContributions extends Disposable implements IWo const value = runner.queryStartTime ? Date.now() - runner.queryStartTime.getTime() : 0; const timeString = parseNumAsTimeString(value, false); this.statusItem.update({ + name: this.name, text: timeString, ariaLabel: timeString }); @@ -101,6 +103,7 @@ export class TimeElapsedStatusBarContributions extends Disposable implements IWo const value = runner.queryStartTime ? Date.now() - runner.queryStartTime.getTime() : 0; const timeString = parseNumAsTimeString(value, false); this.statusItem.update({ + name: this.name, text: timeString, ariaLabel: timeString }); @@ -109,6 +112,7 @@ export class TimeElapsedStatusBarContributions extends Disposable implements IWo ? runner.queryEndTime.getTime() - runner.queryStartTime.getTime() : 0; const timeString = parseNumAsTimeString(value, false); this.statusItem.update({ + name: this.name, text: timeString, ariaLabel: timeString }); @@ -124,6 +128,7 @@ export class RowCountStatusBarContributions extends Disposable implements IWorkb private statusItem: IStatusbarEntryAccessor; private disposable = this._register(new DisposableStore()); + private readonly name = localize('status.query.rowCount', "Row Count"); constructor( @IStatusbarService private readonly statusbarService: IStatusbarService, @@ -133,11 +138,11 @@ export class RowCountStatusBarContributions extends Disposable implements IWorkb super(); this.statusItem = this._register( this.statusbarService.addEntry({ + name: this.name, text: '', ariaLabel: '' }, RowCountStatusBarContributions.ID, - localize('status.query.rowCount', "Row Count"), StatusbarAlignment.RIGHT, 100) ); @@ -201,7 +206,7 @@ export class RowCountStatusBarContributions extends Disposable implements IWorkb return p + cnt; }, 0); const text = localize('rowCount', "{0} rows", rowCount.toLocaleString()); - this.statusItem.update({ text, ariaLabel: text }); + this.statusItem.update({ name: this.name, text: text, ariaLabel: text }); this.show(); } } @@ -220,11 +225,11 @@ export class QueryStatusStatusBarContributions extends Disposable implements IWo super(); this._register( this.statusbarService.addEntry({ + name: localize('status.query.status', "Execution Status"), text: localize('query.status.executing', "Executing query..."), ariaLabel: localize('query.status.executing', "Executing query...") }, QueryStatusStatusBarContributions.ID, - localize('status.query.status', "Execution Status"), StatusbarAlignment.RIGHT, 100) ); @@ -259,6 +264,7 @@ export class QueryStatusStatusBarContributions extends Disposable implements IWo export class QueryResultSelectionSummaryStatusBarContribution extends Disposable implements IWorkbenchContribution { private static readonly ID = 'status.query.selection-summary'; private statusItem: IStatusbarEntryAccessor; + private readonly name = localize('status.query.selection-summary', "Selection Summary"); constructor( @IStatusbarService private readonly statusbarService: IStatusbarService, @@ -269,11 +275,11 @@ export class QueryResultSelectionSummaryStatusBarContribution extends Disposable super(); this.statusItem = this._register( this.statusbarService.addEntry({ + name: this.name, text: '', ariaLabel: '' }, QueryResultSelectionSummaryStatusBarContribution.ID, - localize('status.query.selection-summary', "Selection Summary"), StatusbarAlignment.RIGHT, 100) ); this._register(editorService.onDidActiveEditorChange(() => { this.hide(); }, this)); @@ -302,6 +308,7 @@ export class QueryResultSelectionSummaryStatusBarContribution extends Disposable const sum = numericValues.reduce((previous, current, idx, array) => previous + current); const summaryText = localize('status.query.summaryText', "Average: {0} Count: {1} Sum: {2}", Number((sum / numericValues.length).toFixed(3)), selectedCells.length, sum); this.statusItem.update({ + name: this.name, text: summaryText, ariaLabel: summaryText }); diff --git a/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts b/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts index 06e2b0ade8..e6e06665ef 100644 --- a/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts +++ b/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts @@ -73,8 +73,8 @@ suite('SQL QueryEditor Tests', () => { // Mock EditorDescriptorService to give us a mock editor description let descriptor: IEditorDescriptor = { - getId: function (): string { return 'id'; }, - getName: function (): string { return 'name'; }, + typeId: 'id', + name: 'name', describes: function (obj: any): boolean { return true; }, instantiate(instantiationService: IInstantiationService): EditorPane { return undefined; } }; diff --git a/src/sql/workbench/contrib/query/test/browser/queryInputFactory.test.ts b/src/sql/workbench/contrib/query/test/browser/queryInputFactory.test.ts index 0c04656958..6cb80988ed 100644 --- a/src/sql/workbench/contrib/query/test/browser/queryInputFactory.test.ts +++ b/src/sql/workbench/contrib/query/test/browser/queryInputFactory.test.ts @@ -9,7 +9,7 @@ import { ITestInstantiationService, TestEditorService } from 'vs/workbench/test/ import { URI } from 'vs/base/common/uri'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorInput } from 'vs/workbench/common/editor'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { workbenchInstantiationService } from 'sql/workbench/test/workbenchTestServices'; import { QueryEditorLanguageAssociation } from 'sql/workbench/contrib/query/browser/queryInputFactory'; import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService'; @@ -26,12 +26,13 @@ import { isThenable } from 'vs/base/common/async'; import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService'; import { QueryResultsInput } from 'sql/workbench/common/editor/query/queryResultsInput'; import { extUri } from 'vs/base/common/resources'; +import { IResourceEditorInputIdentifier } from 'vs/platform/editor/common/editor'; suite('Query Input Factory', () => { let instantiationService: ITestInstantiationService; function createFileInput(resource: URI, preferredResource?: URI, preferredMode?: string, preferredName?: string, preferredDescription?: string): FileEditorInput { - return instantiationService.createInstance(FileEditorInput, resource, preferredResource, preferredName, preferredDescription, undefined, preferredMode); + return instantiationService.createInstance(FileEditorInput, resource, preferredResource, preferredName, preferredDescription, undefined, preferredMode, undefined); } test('sync query editor input is connected if global connection exists (OE)', async () => { @@ -100,11 +101,11 @@ suite('Query Input Factory', () => { const queryEditorLanguageAssociation = instantiationService.createInstance(QueryEditorLanguageAssociation); const untitledService = instantiationService.invokeFunction(accessor => accessor.get(IUntitledTextEditorService)); const queryeditorservice = instantiationService.invokeFunction(accessor => accessor.get(IQueryEditorService)); - const newsqlEditorStub = sinon.stub(queryeditorservice, 'newSqlEditor', () => { + const newsqlEditorStub = sinon.stub(queryeditorservice, 'newSqlEditor').callsFake(() => { const untitledInput = instantiationService.createInstance(UntitledTextEditorInput, untitledService.create()); const queryResultsInput: QueryResultsInput = instantiationService.createInstance(QueryResultsInput, untitledInput.resource.toString()); let queryInput = instantiationService.createInstance(UntitledQueryEditorInput, '', untitledInput, queryResultsInput); - return queryInput; + return Promise.resolve(queryInput); }); const input = instantiationService.createInstance(UntitledTextEditorInput, untitledService.create()); const response = queryEditorLanguageAssociation.convertInput(input); @@ -122,7 +123,7 @@ suite('Query Input Factory', () => { instantiationService.stub(IEditorService, editorService); const queryEditorLanguageAssociation = instantiationService.createInstance(QueryEditorLanguageAssociation); const input = createFileInput(URI.file('/test/file.sql'), undefined, undefined, undefined); - queryEditorLanguageAssociation.syncConvertinput(input); + queryEditorLanguageAssociation.syncConvertInput(input); assert(connectionManagementService.numberConnects === 0, 'Convert input should not have been called connect when no global connections exist'); }); @@ -148,12 +149,12 @@ suite('Query Input Factory', () => { const untitledService = instantiationService.invokeFunction(accessor => accessor.get(IUntitledTextEditorService)); const queryeditorservice = instantiationService.invokeFunction(accessor => accessor.get(IQueryEditorService)); const input = instantiationService.createInstance(UntitledTextEditorInput, untitledService.create()); - sinon.stub(editorService, 'isOpened', (editor: IEditorInput) => extUri.isEqual(editor.resource, input.resource)); - const newsqlEditorStub = sinon.stub(queryeditorservice, 'newSqlEditor', () => { + sinon.stub(editorService, 'isOpened').callsFake((editor: IResourceEditorInputIdentifier) => extUri.isEqual(editor.resource, input.resource)); + const newsqlEditorStub = sinon.stub(queryeditorservice, 'newSqlEditor').callsFake(() => { const untitledInput = instantiationService.createInstance(UntitledTextEditorInput, untitledService.create()); const queryResultsInput: QueryResultsInput = instantiationService.createInstance(QueryResultsInput, untitledInput.resource.toString()); let queryInput = instantiationService.createInstance(UntitledQueryEditorInput, '', untitledInput, queryResultsInput); - return queryInput; + return Promise.resolve(queryInput); }); const response = queryEditorLanguageAssociation.convertInput(input); assert(isThenable(response)); diff --git a/src/sql/workbench/contrib/queryPlan/browser/queryPlan.contribution.ts b/src/sql/workbench/contrib/queryPlan/browser/queryPlan.contribution.ts index 5d333e659d..c7b8a82fd4 100644 --- a/src/sql/workbench/contrib/queryPlan/browser/queryPlan.contribution.ts +++ b/src/sql/workbench/contrib/queryPlan/browser/queryPlan.contribution.ts @@ -36,7 +36,7 @@ export class QueryPlanEditorOverrideContribution extends Disposable implements I } private registerEditorOverride(): void { - this._editorOverrideService.registerContributionPoint( + this._editorOverrideService.registerEditor( '*.sqlplan', { id: QueryPlanEditor.ID, diff --git a/src/sql/workbench/contrib/queryPlan/browser/queryPlanEditor.ts b/src/sql/workbench/contrib/queryPlan/browser/queryPlanEditor.ts index bafe30d684..102d70f23b 100644 --- a/src/sql/workbench/contrib/queryPlan/browser/queryPlanEditor.ts +++ b/src/sql/workbench/contrib/queryPlan/browser/queryPlanEditor.ts @@ -5,7 +5,7 @@ import * as DOM from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; -import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -13,6 +13,7 @@ import { QueryPlanInput } from 'sql/workbench/contrib/queryPlan/common/queryPlan import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { QueryPlanView } from 'sql/workbench/contrib/queryPlan/browser/queryPlan'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; export class QueryPlanEditor extends EditorPane { @@ -55,7 +56,7 @@ export class QueryPlanEditor extends EditorPane { this.view.layout(dimension); } - public override async setInput(input: QueryPlanInput, options: EditorOptions, context: IEditorOpenContext): Promise { + public override async setInput(input: QueryPlanInput, options: IEditorOptions, context: IEditorOpenContext): Promise { if (this.input instanceof QueryPlanInput && this.input.matches(input)) { return Promise.resolve(undefined); } diff --git a/src/sql/workbench/contrib/queryPlan/common/queryPlanInput.ts b/src/sql/workbench/contrib/queryPlan/common/queryPlanInput.ts index 821359d6e6..5c71da02cf 100644 --- a/src/sql/workbench/contrib/queryPlan/common/queryPlanInput.ts +++ b/src/sql/workbench/contrib/queryPlan/common/queryPlanInput.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import { EditorInput, EditorModel } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; diff --git a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerEditor.ts b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerEditor.ts index c93c68f994..53ed589260 100644 --- a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerEditor.ts +++ b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerEditor.ts @@ -10,7 +10,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService } from 'vs/platform/storage/common/storage'; import * as DOM from 'vs/base/browser/dom'; -import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -23,6 +23,7 @@ import { fillInActions } from 'vs/platform/actions/browser/menuEntryActionViewIt import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; export type ContextMenuAnchor = HTMLElement | { x: number; y: number; width?: number; height?: number; }; @@ -88,7 +89,7 @@ export class ResourceViewerEditor extends EditorPane { return this._input as ResourceViewerInput; } - override async setInput(input: ResourceViewerInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override async setInput(input: ResourceViewerInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { await super.setInput(input, options, context, token); this._resourceViewerTable.title = input.title; @@ -105,9 +106,9 @@ export class ResourceViewerEditor extends EditorPane { }); this._inputDisposables.add(input.onColumnsChanged(columns => { - this._resourceViewerTable.columns = columns; + this._resourceViewerTable.columns = columns as any; // Cast to any to fix strict type assertion error })); - this._resourceViewerTable.columns = input.columns; + this._resourceViewerTable.columns = input.columns as any; this._inputDisposables.add(input.onDataChanged(() => { this._resourceViewerTable.data = input.data; diff --git a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerTable.ts b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerTable.ts index 6aac97f149..d00abacb4b 100644 --- a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerTable.ts +++ b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerTable.ts @@ -95,7 +95,7 @@ export class ResourceViewerTable extends Disposable { } public set columns(columns: Slick.Column[]) { - this._resourceViewerTable.columns = columns; + this._resourceViewerTable.columns = columns as any; // Cast to any to fix strict type assertion error this._resourceViewerTable.autosizeColumns(); } @@ -128,7 +128,7 @@ export class ResourceViewerTable extends Disposable { const columns = this._resourceViewerTable.grid.getColumns(); let value = true; for (let i = 0; i < columns.length; i++) { - const col: FilterableColumn = columns[i]; + const col: FilterableColumn = columns[i] as any; // Cast to any to fix strict type assertion error if (!col.field) { continue; } diff --git a/src/sql/workbench/contrib/views/browser/treeView.ts b/src/sql/workbench/contrib/views/browser/treeView.ts index 11738c1941..c28a99ef30 100644 --- a/src/sql/workbench/contrib/views/browser/treeView.ts +++ b/src/sql/workbench/contrib/views/browser/treeView.ts @@ -826,9 +826,9 @@ class TreeRenderer extends Disposable implements ITreeRenderer child.name) : []; - const file = files.sort().find(file => strings.startsWith(file.toLowerCase(), 'readme')); + const file = files.sort().find(file => file.toLowerCase().startsWith('readme')); if (file) { return joinPath(folderUri, file); } @@ -100,7 +100,7 @@ export class WelcomePageContribution implements IWorkbenchContribution { arrays.coalesceInPlace(readmes); if (!this.editorService.activeEditor) { if (readmes.length) { - const isMarkDown = (readme: URI) => strings.endsWith(readme.path.toLowerCase(), '.md'); + const isMarkDown = (readme: URI) => readme.path.toLowerCase().endsWith('.md'); await Promise.all([ this.commandService.executeCommand('markdown.showPreview', null, readmes.filter(isMarkDown), { locked: true }), this.editorService.openEditors(readmes.filter(readme => !isMarkDown(readme)).map(readme => ({ resource: readme }))), @@ -666,7 +666,7 @@ class WelcomePage extends Disposable { extensionId: extensionSuggestion.id, }); this.instantiationService.invokeFunction(getInstalledExtensions).then(extensions => { - const installedExtension = arrays.first(extensions, extension => areSameExtensions(extension.identifier, { id: extensionSuggestion.id })); + const installedExtension = extensions.find(extension => areSameExtensions(extension.identifier, { id: extensionSuggestion.id })); if (installedExtension && installedExtension.globallyEnabled) { /* __GDPR__FRAGMENT__ "WelcomePageInstalled-1" : { diff --git a/src/sql/workbench/services/connection/browser/connectionBrowseTab.ts b/src/sql/workbench/services/connection/browser/connectionBrowseTab.ts index 7b9d32495a..8ae66315e1 100644 --- a/src/sql/workbench/services/connection/browser/connectionBrowseTab.ts +++ b/src/sql/workbench/services/connection/browser/connectionBrowseTab.ts @@ -674,9 +674,9 @@ class TreeItemRenderer extends Disposable implements ITreeRenderer 0) { this.flattenGroups(connectionGroupRoot[0], allGroups); diff --git a/src/sql/workbench/services/connection/browser/connectionManagementService.ts b/src/sql/workbench/services/connection/browser/connectionManagementService.ts index df3013a309..6a14fa9ab1 100644 --- a/src/sql/workbench/services/connection/browser/connectionManagementService.ts +++ b/src/sql/workbench/services/connection/browser/connectionManagementService.ts @@ -45,7 +45,6 @@ import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { entries } from 'sql/base/common/collections'; import { values } from 'vs/base/common/collections'; -import { assign } from 'vs/base/common/objects'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -877,7 +876,7 @@ export class ConnectionManagementService extends Disposable implements IConnecti // Request Senders private async sendConnectRequest(connection: interfaces.IConnectionProfile, uri: string): Promise { - let connectionInfo = assign({}, { + let connectionInfo = Object.assign({}, { options: connection.options }); diff --git a/src/sql/workbench/services/connection/browser/connectionWidget.ts b/src/sql/workbench/services/connection/browser/connectionWidget.ts index c996dfa73a..b1a204a346 100644 --- a/src/sql/workbench/services/connection/browser/connectionWidget.ts +++ b/src/sql/workbench/services/connection/browser/connectionWidget.ts @@ -30,7 +30,6 @@ import * as DOM from 'vs/base/browser/dom'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { OS, OperatingSystem } from 'vs/base/common/platform'; import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; -import { endsWith, startsWith } from 'vs/base/common/strings'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; @@ -229,7 +228,7 @@ export class ConnectionWidget extends lifecycle.Disposable { validation: (value: string) => { if (!value) { return ({ type: MessageType.ERROR, content: localize('connectionWidget.missingRequireField', "{0} is required.", serverNameOption.displayName) }); - } else if (startsWith(value, ' ') || endsWith(value, ' ')) { + } else if (value.startsWith(' ') || value.endsWith(' ')) { return ({ type: MessageType.WARNING, content: localize('connectionWidget.fieldWillBeTrimmed', "{0} will be trimmed.", serverNameOption.displayName) }); } return undefined; diff --git a/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts b/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts index 2558b3aeff..bc28bd6c1b 100644 --- a/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts +++ b/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts @@ -33,7 +33,6 @@ import { TestEnvironmentService, TestEditorService } from 'vs/workbench/test/bro import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; -import { assign } from 'vs/base/common/objects'; import { NullAdsTelemetryService } from 'sql/platform/telemetry/common/adsTelemetryService'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -74,9 +73,9 @@ suite('SQL ConnectionManagementService tests', () => { id: undefined }; let connectionProfileWithEmptySavedPassword: IConnectionProfile = - assign({}, connectionProfile, { password: '', serverName: connectionProfile.serverName + 1 }); + Object.assign({}, connectionProfile, { password: '', serverName: connectionProfile.serverName + 1 }); let connectionProfileWithEmptyUnsavedPassword: IConnectionProfile = - assign({}, connectionProfile, { password: '', serverName: connectionProfile.serverName + 2, savePassword: false }); + Object.assign({}, connectionProfile, { password: '', serverName: connectionProfile.serverName + 2, savePassword: false }); let connectionManagementService: ConnectionManagementService; let configResult: { [key: string]: any } = {}; @@ -482,7 +481,7 @@ suite('SQL ConnectionManagementService tests', () => { }); test('changeGroupIdForconnection should change the groupId for a connection profile', async () => { - let profile = assign({}, connectionProfile); + let profile = Object.assign({}, connectionProfile); profile.options = { password: profile.password }; profile.id = 'test_id'; let newGroupId = 'new_group_id'; @@ -504,7 +503,7 @@ suite('SQL ConnectionManagementService tests', () => { }); test('findExistingConnection should find connection for connectionProfile with same info', async () => { - let profile = assign({}, connectionProfile); + let profile = Object.assign({}, connectionProfile); let uri1 = 'connection:connectionId'; let options: IConnectionCompletionOptions = { params: { @@ -535,7 +534,7 @@ suite('SQL ConnectionManagementService tests', () => { }); test('deleteConnection should delete the connection properly', async () => { - let profile = assign({}, connectionProfile); + let profile = Object.assign({}, connectionProfile); let uri1 = 'connection:connectionId'; let options: IConnectionCompletionOptions = { params: { @@ -566,7 +565,7 @@ suite('SQL ConnectionManagementService tests', () => { }); test('deleteConnectionGroup should delete connections in connection group', async () => { - let profile = assign({}, connectionProfile); + let profile = Object.assign({}, connectionProfile); let profileGroup = createConnectionGroup('original_id'); profileGroup.addConnections([profile]); let uri1 = 'connection:connectionId'; @@ -601,13 +600,13 @@ suite('SQL ConnectionManagementService tests', () => { connectionStore.setup(x => x.canChangeConnectionConfig(TypeMoq.It.isAny(), TypeMoq.It.isAnyString())).returns(() => { return true; }); - let profile = assign({}, connectionProfile); + let profile = Object.assign({}, connectionProfile); let newGroupId = 'test_group_id'; assert(connectionManagementService.canChangeConnectionConfig(profile, newGroupId)); }); test('isProfileConnecting should return false for already connected profile', async () => { - let profile = assign({}, connectionProfile); + let profile = Object.assign({}, connectionProfile); let uri = 'Editor Uri'; let options: IConnectionCompletionOptions = { params: { @@ -635,7 +634,7 @@ suite('SQL ConnectionManagementService tests', () => { }); test('disconnect should disconnect the profile when given ConnectionProfile', async () => { - let profile = assign({}, connectionProfile); + let profile = Object.assign({}, connectionProfile); let uri = 'connection:connectionId'; // must use default connection uri for test to work. let options: IConnectionCompletionOptions = { params: { @@ -664,7 +663,7 @@ suite('SQL ConnectionManagementService tests', () => { }); test('disconnect should disconnect the profile when given uri string', async () => { - let profile = assign({}, connectionProfile); + let profile = Object.assign({}, connectionProfile); let uri = 'connection:connectionId'; // must use default connection uri for test to work. let options: IConnectionCompletionOptions = { params: { @@ -693,7 +692,7 @@ suite('SQL ConnectionManagementService tests', () => { }); test('cancelConnection should disconnect the profile', async () => { - let profile = assign({}, connectionProfile); + let profile = Object.assign({}, connectionProfile); let uri = 'connection:connectionId'; // must use default connection uri for test to work. let options: IConnectionCompletionOptions = { params: { @@ -751,7 +750,7 @@ suite('SQL ConnectionManagementService tests', () => { }); test('getConnection should grab connection that is connected', async () => { - let profile = assign({}, connectionProfile); + let profile = Object.assign({}, connectionProfile); let uri = 'connection:connectionId'; // must use default connection uri for test to work. let badString = 'bad_string'; let options: IConnectionCompletionOptions = { @@ -784,7 +783,7 @@ suite('SQL ConnectionManagementService tests', () => { }); test('connectIfNotConnected should not try to connect with already connected profile', async () => { - let profile = assign({}, connectionProfile); + let profile = Object.assign({}, connectionProfile); let uri = 'connection:connectionId'; // must use default connection uri for test to work. let options: IConnectionCompletionOptions = { params: { @@ -818,7 +817,7 @@ suite('SQL ConnectionManagementService tests', () => { }); test('getConnectionString should get connection string of connectionId', async () => { - let profile = assign({}, connectionProfile); + let profile = Object.assign({}, connectionProfile); let uri = 'connection:connectionId'; // must use default connection uri for test to work. let badString = 'bad_string'; let options: IConnectionCompletionOptions = { @@ -858,7 +857,7 @@ suite('SQL ConnectionManagementService tests', () => { test('rebuildIntellisenseCache should call rebuildIntelliSenseCache on provider', async () => { - let profile = assign({}, connectionProfile); + let profile = Object.assign({}, connectionProfile); let uri = 'connection:connectionId'; // must use default connection uri for test to work. let options: IConnectionCompletionOptions = { params: { @@ -893,7 +892,7 @@ suite('SQL ConnectionManagementService tests', () => { }); test('buildConnectionInfo should get connection string of connectionId', async () => { - let profile = assign({}, connectionProfile); + let profile = Object.assign({}, connectionProfile); let uri = 'connection:connectionId'; // must use default connection uri for test to work. let options: IConnectionCompletionOptions = { params: { @@ -928,9 +927,9 @@ suite('SQL ConnectionManagementService tests', () => { }); test('removeConnectionProfileCredentials should return connection profile without password', () => { - let profile = assign({}, connectionProfile); + let profile = Object.assign({}, connectionProfile); connectionStore.setup(x => x.getProfileWithoutPassword(TypeMoq.It.isAny())).returns(() => { - let profileWithoutPass = assign({}, connectionProfile); + let profileWithoutPass = Object.assign({}, connectionProfile); profileWithoutPass.password = undefined; return profileWithoutPass; }); @@ -939,7 +938,7 @@ suite('SQL ConnectionManagementService tests', () => { }); test('getConnectionProfileById should return profile when given profileId', async () => { - let profile = assign({}, connectionProfile); + let profile = Object.assign({}, connectionProfile); let uri = 'connection:connectionId'; // must use default connection uri for test to work. let badString = 'bad_string'; let options: IConnectionCompletionOptions = { @@ -973,7 +972,7 @@ suite('SQL ConnectionManagementService tests', () => { }); test('Edit Connection - Changing connection profile name for same URI should persist after edit', async () => { - let profile = assign({}, connectionProfile); + let profile = Object.assign({}, connectionProfile); let uri1 = 'test_uri1'; let newname = 'connection renamed'; let options: IConnectionCompletionOptions = { @@ -998,7 +997,7 @@ suite('SQL ConnectionManagementService tests', () => { }; await connect(uri1, options, true, profile); - let newProfile = assign({}, connectionProfile); + let newProfile = Object.assign({}, connectionProfile); newProfile.connectionName = newname; options.params.isEditConnection = true; await connect(uri1, options, true, newProfile); @@ -1008,7 +1007,7 @@ suite('SQL ConnectionManagementService tests', () => { test('Edit Connection - Connecting a different URI with same profile via edit should not change profile ID.', async () => { let uri1 = 'test_uri1'; let uri2 = 'test_uri2'; - let profile = assign({}, connectionProfile); + let profile = Object.assign({}, connectionProfile); profile.id = '0451'; let options: IConnectionCompletionOptions = { params: { @@ -1320,7 +1319,7 @@ suite('SQL ConnectionManagementService tests', () => { }); test('registerIconProvider should register icon provider for connectionManagementService', async () => { - let profile = assign({}, connectionProfile); + let profile = Object.assign({}, connectionProfile); let serverInfo: azdata.ServerInfo = { serverMajorVersion: 0, serverMinorVersion: 0, @@ -1464,9 +1463,9 @@ suite('SQL ConnectionManagementService tests', () => { let dbName = 'master'; let serverName = 'test_server'; let userName = 'test_user'; - let connectionProfileWithoutDb: IConnectionProfile = assign(connectionProfile, + let connectionProfileWithoutDb: IConnectionProfile = Object.assign(connectionProfile, { serverName: serverName, databaseName: '', userName: userName, getOptionsKey: () => undefined }); - let connectionProfileWithDb: IConnectionProfile = assign(connectionProfileWithoutDb, { databaseName: dbName }); + let connectionProfileWithDb: IConnectionProfile = Object.assign(connectionProfileWithoutDb, { databaseName: dbName }); // Save the database with a URI that has the database name filled in, to mirror Carbon's behavior let ownerUri = Utils.generateUri(connectionProfileWithDb); await connect(ownerUri, undefined, false, connectionProfileWithoutDb); @@ -1486,9 +1485,9 @@ suite('SQL ConnectionManagementService tests', () => { let newDbName = 'renamed_master'; let serverName = 'test_server'; let userName = 'test_user'; - let connectionProfileWithoutDb: IConnectionProfile = assign(connectionProfile, + let connectionProfileWithoutDb: IConnectionProfile = Object.assign(connectionProfile, { serverName: serverName, databaseName: '', userName: userName, getOptionsKey: () => undefined }); - let connectionProfileWithDb: IConnectionProfile = assign(connectionProfileWithoutDb, { databaseName: dbName }); + let connectionProfileWithDb: IConnectionProfile = Object.assign(connectionProfileWithoutDb, { databaseName: dbName }); // Save the database with a URI that has the database name filled in, to mirror Carbon's behavior let ownerUri = Utils.generateUri(connectionProfileWithDb); let listDatabasesThenable = (connectionUri: string) => { @@ -1545,7 +1544,7 @@ suite('SQL ConnectionManagementService tests', () => { }); test('getConnectionCredentials returns the credentials dictionary for an active connection profile', async () => { - let profile = assign({}, connectionProfile); + let profile = Object.assign({}, connectionProfile); profile.options = { password: profile.password }; profile.id = 'test_id'; connectionStatusManager.addConnection(profile, 'test_uri'); @@ -1607,7 +1606,7 @@ suite('SQL ConnectionManagementService tests', () => { }); test('getConnectionUriFromId returns a URI of an active connection with the given id', () => { - let profile = assign({}, connectionProfile); + let profile = Object.assign({}, connectionProfile); profile.options = { password: profile.password }; profile.id = 'test_id'; let uri = 'test_initial_uri'; @@ -1626,7 +1625,7 @@ suite('SQL ConnectionManagementService tests', () => { }); test('getConectionUriFromId returns undefined if the given connection is not active', () => { - let profile = assign({}, connectionProfile); + let profile = Object.assign({}, connectionProfile); profile.options = { password: profile.password }; profile.id = 'test_id'; connectionStatusManager.addConnection(profile, Utils.generateUri(profile)); diff --git a/src/sql/workbench/services/connection/test/browser/testTreeView.ts b/src/sql/workbench/services/connection/test/browser/testTreeView.ts index 87f6d40d2d..d90227352f 100644 --- a/src/sql/workbench/services/connection/test/browser/testTreeView.ts +++ b/src/sql/workbench/services/connection/test/browser/testTreeView.ts @@ -871,7 +871,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer { @@ -60,11 +60,12 @@ suite('Insights Utils tests', function () { undefined, new TestContextService(), undefined, + undefined, undefined); const fileService = new class extends TestFileService { override exists(uri: URI): Promise { - return pfs.exists(uri.fsPath); + return pfs.Promises.exists(uri.fsPath); } }; @@ -92,11 +93,12 @@ suite('Insights Utils tests', function () { undefined, contextService, undefined, + undefined, undefined); const fileService = new class extends TestFileService { override exists(uri: URI): Promise { - return pfs.exists(uri.fsPath); + return pfs.Promises.exists(uri.fsPath); } }; @@ -124,11 +126,12 @@ suite('Insights Utils tests', function () { undefined, contextService, undefined, + undefined, undefined); const fileService = new class extends TestFileService { override exists(uri: URI): Promise { - return pfs.exists(uri.fsPath); + return pfs.Promises.exists(uri.fsPath); } }; @@ -158,11 +161,12 @@ suite('Insights Utils tests', function () { undefined, contextService, undefined, + undefined, undefined); const fileService = new class extends TestFileService { override exists(uri: URI): Promise { - return pfs.exists(uri.fsPath); + return pfs.Promises.exists(uri.fsPath); } }; @@ -193,11 +197,12 @@ suite('Insights Utils tests', function () { undefined, undefined, undefined, + undefined, undefined); const fileService = new class extends TestFileService { override exists(uri: URI): Promise { - return pfs.exists(uri.fsPath); + return pfs.Promises.exists(uri.fsPath); } }; @@ -223,11 +228,12 @@ suite('Insights Utils tests', function () { undefined, undefined, undefined, + undefined, undefined); const fileService = new class extends TestFileService { override exists(uri: URI): Promise { - return pfs.exists(uri.fsPath); + return pfs.Promises.exists(uri.fsPath); } }; @@ -248,11 +254,12 @@ suite('Insights Utils tests', function () { undefined, undefined, undefined, + undefined, undefined); const fileService = new class extends TestFileService { override exists(uri: URI): Promise { - return pfs.exists(uri.fsPath); + return pfs.Promises.exists(uri.fsPath); } }; diff --git a/src/sql/workbench/services/languageAssociation/common/doHandleUpgrade.ts b/src/sql/workbench/services/languageAssociation/common/doHandleUpgrade.ts deleted file mode 100644 index ae44958348..0000000000 --- a/src/sql/workbench/services/languageAssociation/common/doHandleUpgrade.ts +++ /dev/null @@ -1,30 +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 { EditorInput } from 'vs/workbench/common/editor'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; -import { Extensions as ILanguageAssociationExtensions, ILanguageAssociationRegistry } from 'sql/workbench/services/languageAssociation/common/languageAssociation'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; - -const languageRegistry = Registry.as(ILanguageAssociationExtensions.LanguageAssociations); - -export function doHandleUpgrade(editor?: EditorInput): EditorInput | undefined { - if (editor instanceof UntitledTextEditorInput || editor instanceof FileEditorInput) { - let language: string | undefined; - if (editor instanceof UntitledTextEditorInput) { - language = editor.getMode(); - } else { - language = editor.getPreferredMode(); - } - if (language) { - const association = languageRegistry.getAssociationForLanguage(language); - if (association && association.syncConvertinput) { - return association.syncConvertinput(editor); - } - } - } - return editor; -} diff --git a/src/sql/workbench/services/languageAssociation/common/languageAssociation.ts b/src/sql/workbench/services/languageAssociation/common/languageAssociation.ts index 8887c367d0..eff2c57d49 100644 --- a/src/sql/workbench/services/languageAssociation/common/languageAssociation.ts +++ b/src/sql/workbench/services/languageAssociation/common/languageAssociation.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Registry } from 'vs/platform/registry/common/platform'; -import { IEditorInput, EditorInput } from 'vs/workbench/common/editor'; +import { IEditorInput } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { ServicesAccessor, IInstantiationService, BrandedService } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -13,11 +14,7 @@ export type BaseInputCreator = (activeEditor: IEditorInput) => IEditorInput; export interface ILanguageAssociation { convertInput(activeEditor: IEditorInput): Promise | EditorInput | undefined; - /** - * Used for scenarios when we need to synchrounly create inputs, currently only for handling upgrades - * and planned to be removed eventually - */ - syncConvertinput?(activeEditor: IEditorInput): EditorInput | undefined; + syncConvertInput?(activeEditor: IEditorInput): EditorInput | undefined; createBase(activeEditor: IEditorInput): IEditorInput; } diff --git a/src/sql/workbench/services/notebook/browser/models/notebookModel.ts b/src/sql/workbench/services/notebook/browser/models/notebookModel.ts index 4bd5aee876..f8cd0f4ebc 100644 --- a/src/sql/workbench/services/notebook/browser/models/notebookModel.ts +++ b/src/sql/workbench/services/notebook/browser/models/notebookModel.ts @@ -24,7 +24,6 @@ import { ConnectionProfile } from 'sql/platform/connection/common/connectionProf import { uriPrefixes } from 'sql/platform/connection/common/utils'; import { ILogService } from 'vs/platform/log/common/log'; import { getErrorMessage } from 'vs/base/common/errors'; -import { startsWith } from 'vs/base/common/strings'; import { notebookConstants } from 'sql/workbench/services/notebook/browser/interfaces'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; import { Deferred } from 'sql/base/common/promise'; @@ -1085,7 +1084,7 @@ export class NotebookModel extends Disposable implements INotebookModel { if (this._savedKernelInfo.display_name !== displayName) { this._savedKernelInfo.display_name = displayName; } - let standardKernel = this._standardKernels.find(kernel => kernel.displayName === displayName || startsWith(displayName, kernel.displayName)); + let standardKernel = this._standardKernels.find(kernel => kernel.displayName === displayName || displayName.startsWith(kernel.displayName)); if (standardKernel && this._savedKernelInfo.name && this._savedKernelInfo.name !== standardKernel.name) { this._savedKernelInfo.name = standardKernel.name; this._savedKernelInfo.display_name = standardKernel.displayName; diff --git a/src/sql/workbench/services/notebook/browser/models/notebookUtils.ts b/src/sql/workbench/services/notebook/browser/models/notebookUtils.ts index 31b5024b39..09f2d274c0 100644 --- a/src/sql/workbench/services/notebook/browser/models/notebookUtils.ts +++ b/src/sql/workbench/services/notebook/browser/models/notebookUtils.ts @@ -7,8 +7,6 @@ import * as path from 'vs/base/common/path'; import { nb, ServerInfo } from 'azdata'; import { DEFAULT_NOTEBOOK_PROVIDER, DEFAULT_NOTEBOOK_FILETYPE, INotebookService } from 'sql/workbench/services/notebook/browser/notebookService'; import { URI } from 'vs/base/common/uri'; -import { startsWith } from 'vs/base/common/strings'; -import { assign } from 'vs/base/common/objects'; export const clusterEndpointsProperty = 'clusterEndpoints'; export const hadoopEndpointNameGateway = 'gateway'; @@ -23,7 +21,7 @@ export function getProvidersForFileName(fileName: string, notebookService: INote let fileExt = path.extname(fileName); let providers: string[]; // First try to get provider for actual file type - if (fileExt && startsWith(fileExt, '.')) { + if (fileExt && fileExt.startsWith('.')) { fileExt = fileExt.slice(1, fileExt.length); providers = notebookService.getProvidersForFileType(fileExt); } @@ -44,7 +42,7 @@ export function getStandardKernelsForProvider(providerId: string, notebookServic } let standardKernels = notebookService.getStandardKernelsForProvider(providerId); standardKernels.forEach(kernel => { - assign(kernel, { + Object.assign(kernel, { name: kernel.name, connectionProviderIds: kernel.connectionProviderIds, notebookProvider: providerId diff --git a/src/sql/workbench/services/notebook/browser/notebookViews/autodash.ts b/src/sql/workbench/services/notebook/browser/notebookViews/autodash.ts index 253fc6395c..6a10dc5b34 100644 --- a/src/sql/workbench/services/notebook/browser/notebookViews/autodash.ts +++ b/src/sql/workbench/services/notebook/browser/notebookViews/autodash.ts @@ -87,11 +87,11 @@ class CellDisplayGroup extends DisplayGroup { } hasGraph(cell: ICellModel): boolean { - return !!cell.outputs.find((o: nb.IDisplayResult) => o?.output_type === 'display_data' && o?.data.hasOwnProperty('application/vnd.plotly.v1+json')); + return !!cell.outputs.find((o: nb.ICellOutput) => o?.output_type === 'display_data' && (o as nb.IDisplayResult)?.data.hasOwnProperty('application/vnd.plotly.v1+json')); } hasTable(cell: ICellModel): boolean { - return !!cell.outputs.find((o: nb.IDisplayResult) => o?.output_type === 'display_data' && o?.data.hasOwnProperty('application/vnd.dataresource+json')); + return !!cell.outputs.find((o: nb.ICellOutput) => o?.output_type === 'display_data' && (o as nb.IDisplayResult)?.data.hasOwnProperty('application/vnd.dataresource+json')); } } diff --git a/src/sql/workbench/services/notebook/browser/sql/sqlSessionManager.ts b/src/sql/workbench/services/notebook/browser/sql/sqlSessionManager.ts index 23a99b0710..39e5bdb99b 100644 --- a/src/sql/workbench/services/notebook/browser/sql/sqlSessionManager.ts +++ b/src/sql/workbench/services/notebook/browser/sql/sqlSessionManager.ts @@ -24,7 +24,6 @@ import { ILanguageMagic } from 'sql/workbench/services/notebook/browser/notebook import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; import { URI } from 'vs/base/common/uri'; import { getUriPrefix, uriPrefixes } from 'sql/platform/connection/common/utils'; -import { startsWith } from 'vs/base/common/strings'; import { onUnexpectedError } from 'vs/base/common/errors'; import { FutureInternal, notebookConstants } from 'sql/workbench/services/notebook/browser/interfaces'; import { tryMatchCellMagic } from 'sql/workbench/services/notebook/browser/utils'; @@ -357,7 +356,7 @@ class SqlKernel extends Disposable implements nb.IKernel { let code = Array.isArray(content.code) ? content.code.join('') : content.code; let firstLineEnd = code.indexOf(this.textResourcePropertiesService.getEOL(URI.file(this._path))); let firstLine = code.substring(0, (firstLineEnd >= 0) ? firstLineEnd : 0).trimLeft(); - if (startsWith(firstLine, '%%')) { + if (firstLine.startsWith('%%')) { // Strip out the line code = code.substring(firstLineEnd, code.length); // Try and match to an external script magic. If we add more magics later, should handle transforms better diff --git a/src/sql/workbench/services/objectExplorer/browser/objectExplorerActions.ts b/src/sql/workbench/services/objectExplorer/browser/objectExplorerActions.ts index 0308639600..2858970018 100644 --- a/src/sql/workbench/services/objectExplorer/browser/objectExplorerActions.ts +++ b/src/sql/workbench/services/objectExplorer/browser/objectExplorerActions.ts @@ -42,7 +42,7 @@ export class OEAction extends ExecuteCommandAction { super(id, label, commandService); } - public override async run(actionContext: any): Promise { + public override async run(actionContext: any): Promise { const treeSelectionHandler = this._instantiationService.createInstance(TreeSelectionHandler); let profile: IConnectionProfile | undefined = undefined; @@ -61,9 +61,7 @@ export class OEAction extends ExecuteCommandAction { if (profile) { return super.run(profile).then(() => { treeSelectionHandler.onTreeActionStateChange(false); - return true; }); } - return false; } } diff --git a/src/sql/workbench/services/objectExplorer/browser/objectExplorerService.ts b/src/sql/workbench/services/objectExplorer/browser/objectExplorerService.ts index 28da3153ac..11965457c1 100644 --- a/src/sql/workbench/services/objectExplorer/browser/objectExplorerService.ts +++ b/src/sql/workbench/services/objectExplorer/browser/objectExplorerService.ts @@ -18,7 +18,6 @@ import * as Utils from 'sql/platform/connection/common/utils'; import { ILogService } from 'vs/platform/log/common/log'; import { entries } from 'sql/base/common/collections'; import { values } from 'vs/base/common/collections'; -import { startsWith } from 'vs/base/common/strings'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; import { ServerTreeActionProvider } from 'sql/workbench/services/objectExplorer/browser/serverTreeActionProvider'; import { ITree } from 'vs/base/parts/tree/browser/tree'; @@ -842,7 +841,7 @@ export class ObjectExplorerService implements IObjectExplorerService { } if (currentNode.children) { // Look at the next node in the path, which is the child object with the longest path where the desired path starts with the child path - let children = currentNode.children.filter(child => startsWith(nodePath, child.nodePath)); + let children = currentNode.children.filter(child => nodePath.startsWith(child.nodePath)); if (children.length > 0) { nextNode = children.reduce((currentMax, candidate) => currentMax.nodePath.length < candidate.nodePath.length ? candidate : currentMax); } diff --git a/src/sql/workbench/services/profiler/browser/profilerFilter.ts b/src/sql/workbench/services/profiler/browser/profilerFilter.ts index 7cc5fb73e7..4c7f35263e 100644 --- a/src/sql/workbench/services/profiler/browser/profilerFilter.ts +++ b/src/sql/workbench/services/profiler/browser/profilerFilter.ts @@ -4,8 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { ProfilerFilterClause, ProfilerFilter, ProfilerFilterClauseOperator } from 'sql/workbench/services/profiler/browser/interfaces'; -import { startsWith } from 'vs/base/common/strings'; - export function FilterData(filter: ProfilerFilter, data: any[]): any[] { if (!data || !filter) { @@ -74,10 +72,10 @@ function matches(item: any, clauses: ProfilerFilterClause[]): boolean { match = !actualValueString || !(actualValueString.indexOf(expectedValueString) > -1); break; case ProfilerFilterClauseOperator.StartsWith: - match = startsWith(actualValueString, expectedValueString); + match = actualValueString.startsWith(expectedValueString); break; case ProfilerFilterClauseOperator.NotStartsWith: - match = !actualValueString || !startsWith(actualValueString, expectedValueString); + match = !actualValueString || !actualValueString.startsWith(expectedValueString); break; default: throw new Error(`Not a valid operator: ${clause.operator}`); diff --git a/src/sql/workbench/services/query/common/queryManagement.ts b/src/sql/workbench/services/query/common/queryManagement.ts index 4aa095ec75..0f71d562ce 100644 --- a/src/sql/workbench/services/query/common/queryManagement.ts +++ b/src/sql/workbench/services/query/common/queryManagement.ts @@ -10,7 +10,6 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import * as azdata from 'azdata'; import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; import { Event, Emitter } from 'vs/base/common/event'; -import { assign } from 'vs/base/common/objects'; import { IAdsTelemetryService, ITelemetryEventProperties } from 'sql/platform/telemetry/common/telemetry'; import EditQueryRunner from 'sql/workbench/services/editData/common/editQueryRunner'; import { IRange, Range } from 'vs/editor/common/core/range'; @@ -188,7 +187,7 @@ export class QueryManagementService implements IQueryManagementService { provider: providerId, }; if (runOptions) { - assign(data, { + Object.assign(data, { displayEstimatedQueryPlan: runOptions.displayEstimatedQueryPlan, displayActualQueryPlan: runOptions.displayActualQueryPlan }); diff --git a/src/sql/workbench/services/queryEditor/browser/editorDescriptorService.ts b/src/sql/workbench/services/queryEditor/browser/editorDescriptorService.ts index dde82ff435..d1c1ce96b0 100644 --- a/src/sql/workbench/services/queryEditor/browser/editorDescriptorService.ts +++ b/src/sql/workbench/services/queryEditor/browser/editorDescriptorService.ts @@ -3,7 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorExtensions, EditorInput } from 'vs/workbench/common/editor'; +import { EditorExtensions } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IEditorDescriptor, IEditorRegistry } from 'vs/workbench/browser/editor'; import { Registry } from 'vs/platform/registry/common/platform'; diff --git a/src/sql/workbench/services/queryEditor/test/electron-browser/queryEditorService.test.ts b/src/sql/workbench/services/queryEditor/test/electron-browser/queryEditorService.test.ts index 30cbd770e1..0d16c493ab 100644 --- a/src/sql/workbench/services/queryEditor/test/electron-browser/queryEditorService.test.ts +++ b/src/sql/workbench/services/queryEditor/test/electron-browser/queryEditorService.test.ts @@ -16,8 +16,8 @@ suite('Query Editor Service', () => { const instantiationService = workbenchInstantiationService(); const editorService = instantiationService.invokeFunction(accessor => accessor.get(IEditorService)); const untitledService = instantiationService.invokeFunction(accessor => accessor.get(IUntitledTextEditorService)); - const openStub = sinon.stub(editorService, 'openEditor', () => Promise.resolve()); - sinon.stub(editorService, 'createEditorInput', () => instantiationService.createInstance(UntitledTextEditorInput, untitledService.create())); + const openStub = sinon.stub(editorService, 'openEditor').callsFake(() => Promise.resolve(undefined)); + sinon.stub(editorService, 'createEditorInput').callsFake(() => instantiationService.createInstance(UntitledTextEditorInput, untitledService.create())); const queryEditorService = instantiationService.createInstance(QueryEditorService); await queryEditorService.newSqlEditor({ open: true }); @@ -29,8 +29,8 @@ suite('Query Editor Service', () => { const instantiationService = workbenchInstantiationService(); const editorService = instantiationService.invokeFunction(accessor => accessor.get(IEditorService)); const untitledService = instantiationService.invokeFunction(accessor => accessor.get(IUntitledTextEditorService)); - const openStub = sinon.stub(editorService, 'openEditor', () => Promise.resolve()); - sinon.stub(editorService, 'createEditorInput', () => instantiationService.createInstance(UntitledTextEditorInput, untitledService.create())); + const openStub = sinon.stub(editorService, 'openEditor').callsFake(() => Promise.resolve(undefined)); + sinon.stub(editorService, 'createEditorInput').callsFake(() => instantiationService.createInstance(UntitledTextEditorInput, untitledService.create())); const queryEditorService = instantiationService.createInstance(QueryEditorService); await queryEditorService.newSqlEditor(); @@ -42,8 +42,8 @@ suite('Query Editor Service', () => { const instantiationService = workbenchInstantiationService(); const editorService = instantiationService.invokeFunction(accessor => accessor.get(IEditorService)); const untitledService = instantiationService.invokeFunction(accessor => accessor.get(IUntitledTextEditorService)); - const openStub = sinon.stub(editorService, 'openEditor', () => Promise.resolve()); - sinon.stub(editorService, 'createEditorInput', () => instantiationService.createInstance(UntitledTextEditorInput, untitledService.create())); + const openStub = sinon.stub(editorService, 'openEditor').callsFake(() => Promise.resolve(undefined)); + sinon.stub(editorService, 'createEditorInput').callsFake(() => instantiationService.createInstance(UntitledTextEditorInput, untitledService.create())); const queryEditorService = instantiationService.createInstance(QueryEditorService); await queryEditorService.newSqlEditor({ open: false }); diff --git a/src/sql/workbench/test/browser/parts/editor/editorStatusModeSelect.test.ts b/src/sql/workbench/test/browser/parts/editor/editorStatusModeSelect.test.ts index 2774e3368f..95e4177887 100644 --- a/src/sql/workbench/test/browser/parts/editor/editorStatusModeSelect.test.ts +++ b/src/sql/workbench/test/browser/parts/editor/editorStatusModeSelect.test.ts @@ -19,12 +19,13 @@ import { TestQueryEditorService } from 'sql/workbench/services/queryEditor/test/ import { ITestInstantiationService, TestEditorService } from 'vs/workbench/test/browser/workbenchTestServices'; import { NotebookServiceStub } from 'sql/workbench/contrib/notebook/test/stubs'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IUntitledTextResourceEditorInput, EditorInput, IVisibleEditorPane } from 'vs/workbench/common/editor'; +import { IUntitledTextResourceEditorInput, IVisibleEditorPane } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { URI } from 'vs/base/common/uri'; -import { FileQueryEditorInput } from 'sql/workbench/contrib/query/common/fileQueryEditorInput'; +import { FileQueryEditorInput } from 'sql/workbench/contrib/query/browser/fileQueryEditorInput'; import { QueryResultsInput } from 'sql/workbench/common/editor/query/queryResultsInput'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorType } from 'vs/editor/common/editorCommon'; @@ -39,7 +40,7 @@ suite('set mode', () => { let disposables: IDisposable[] = []; function createFileInput(resource: URI, preferredResource?: URI, preferredMode?: string, preferredName?: string, preferredDescription?: string): FileEditorInput { - return instantiationService.createInstance(FileEditorInput, resource, preferredResource, preferredName, preferredDescription, undefined, preferredMode); + return instantiationService.createInstance(FileEditorInput, resource, preferredResource, preferredName, preferredDescription, undefined, preferredMode, undefined); } setup(() => { @@ -62,7 +63,7 @@ suite('set mode', () => { test('does leave editor alone and change mode when changed from plaintext to json', async () => { const editorService = new MockEditorService(instantiationService, 'plaintext'); instantiationService.stub(IEditorService, editorService); - const replaceEditorStub = sinon.stub(editorService, 'replaceEditors', () => Promise.resolve()); + const replaceEditorStub = sinon.stub(editorService, 'replaceEditors').callsFake(() => Promise.resolve()); const stub = sinon.stub(); const modeSupport = { setMode: stub }; const activeEditor = createFileInput(URI.file('/test/file.txt'), undefined, 'plaintext', undefined); @@ -122,7 +123,7 @@ suite('set mode', () => { const stub = sinon.stub(); const modeSupport = { setMode: stub }; const activeEditor = createFileInput(URI.file('/test/file.txt'), undefined, 'plaintext', undefined); - sinon.stub(activeEditor, 'isDirty', () => true); + sinon.stub(activeEditor, 'isDirty').callsFake(() => true); await instantiationService.invokeFunction(setMode, modeSupport, activeEditor, 'sql'); assert(stub.notCalled); assert(errorStub.calledOnce); diff --git a/src/sql/workbench/test/browser/taskUtilities.test.ts b/src/sql/workbench/test/browser/taskUtilities.test.ts index 56287456c9..b06ad9073e 100644 --- a/src/sql/workbench/test/browser/taskUtilities.test.ts +++ b/src/sql/workbench/test/browser/taskUtilities.test.ts @@ -11,7 +11,6 @@ import { TestConnectionManagementService } from 'sql/platform/connection/test/co import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; import { TestEditorService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { assign } from 'vs/base/common/objects'; suite('TaskUtilities', function () { test('getCurrentGlobalConnection returns the selected OE server if a server or one of its children is selected', () => { @@ -61,7 +60,7 @@ suite('TaskUtilities', function () { let mockConnectionManagementService = TypeMoq.Mock.ofType(TestConnectionManagementService); let mockWorkbenchEditorService = TypeMoq.Mock.ofType(TestEditorService); let oeProfile = new ConnectionProfile(undefined, connectionProfile); - let connectionProfile2 = assign({}, connectionProfile); + let connectionProfile2 = Object.assign({}, connectionProfile); connectionProfile2.serverName = 'test_server_2'; connectionProfile2.id = 'test_id_2'; let tabProfile = new ConnectionProfile(undefined, connectionProfile2); diff --git a/src/sql/workbench/test/electron-browser/api/extHostModelView.test.ts b/src/sql/workbench/test/electron-browser/api/extHostModelView.test.ts index 8ee7576f7a..4b07f7db18 100644 --- a/src/sql/workbench/test/electron-browser/api/extHostModelView.test.ts +++ b/src/sql/workbench/test/electron-browser/api/extHostModelView.test.ts @@ -11,7 +11,6 @@ import { MainThreadModelViewShape } from 'sql/workbench/api/common/sqlExtHost.pr import { IMainContext } from 'vs/workbench/api/common/extHost.protocol'; import { IComponentShape, IItemConfig, ComponentEventType, IComponentEventArgs, ModelComponentTypes, DeclarativeDataType } from 'sql/workbench/api/common/sqlExtHostTypes'; import { TitledFormItemLayout } from 'sql/workbench/browser/modelComponents/formContainer.component'; -import { assign } from 'vs/base/common/objects'; interface InternalItemConfig { toIItemConfig(): IItemConfig; @@ -178,7 +177,7 @@ suite('ExtHostModelView Validation Tests', () => { topLevelInputFormComponent, { components: [ - assign(groupInputFormComponent, { layout: groupInputLayout }), + Object.assign(groupInputFormComponent, { layout: groupInputLayout }), groupDropdownFormComponent ], title: groupTitle diff --git a/src/sql/workbench/test/electron-browser/api/mainThreadNotebook.test.ts b/src/sql/workbench/test/electron-browser/api/mainThreadNotebook.test.ts index 4bd16a484e..135260b4ff 100644 --- a/src/sql/workbench/test/electron-browser/api/mainThreadNotebook.test.ts +++ b/src/sql/workbench/test/electron-browser/api/mainThreadNotebook.test.ts @@ -30,7 +30,7 @@ suite('MainThreadNotebook Tests', () => { let providerId = 'TestProvider'; setup(() => { - mockProxy = TypeMoq.Mock.ofType(ExtHostNotebookStub); + mockProxy = TypeMoq.Mock.ofType(ExtHostNotebookStub); let extContext = { getProxy: proxyType => mockProxy.object }; diff --git a/src/tsconfig.base.json b/src/tsconfig.base.json index f066c31153..173cdaea15 100644 --- a/src/tsconfig.base.json +++ b/src/tsconfig.base.json @@ -12,8 +12,10 @@ "strictBindCallApply": true, "strictNullChecks": false, "strictPropertyInitialization": false, - "strict": false, "allowUnreachableCode": false, + "strict": true, + "strictOptionalProperties": false, + "useUnknownInCatchVariables": false, "forceConsistentCasingInFileNames": true, "baseUrl": ".", "paths": { diff --git a/src/tsec.exemptions.json b/src/tsec.exemptions.json index 21ca7d78cc..c82a94b0b1 100644 --- a/src/tsec.exemptions.json +++ b/src/tsec.exemptions.json @@ -20,6 +20,7 @@ "vs/editor/browser/view/domLineBreaksComputer.ts", "vs/editor/browser/view/viewLayer.ts", "vs/editor/browser/widget/diffEditorWidget.ts", + "vs/editor/contrib/inlineCompletions/ghostTextWidget.ts", "vs/editor/browser/widget/diffReview.ts", "vs/editor/standalone/browser/colorizer.ts", "vs/workbench/api/worker/extHostExtensionService.ts", diff --git a/src/vs/base/browser/browser.ts b/src/vs/base/browser/browser.ts index bb595ef674..68ef051db8 100644 --- a/src/vs/base/browser/browser.ts +++ b/src/vs/base/browser/browser.ts @@ -28,7 +28,7 @@ class WindowManager { } this._zoomLevel = zoomLevel; - // See https://github.com/Microsoft/vscode/issues/26151 + // See https://github.com/microsoft/vscode/issues/26151 this._lastZoomLevelChangeTime = isTrusted ? 0 : Date.now(); this._onDidChangeZoomLevel.fire(this._zoomLevel); } @@ -115,7 +115,6 @@ 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 isEdgeLegacyWebView = (userAgent.indexOf('Edge/') >= 0) && (userAgent.indexOf('WebView/') >= 0); export const isElectron = (userAgent.indexOf('Electron/') >= 0); export const isAndroid = (userAgent.indexOf('Android') >= 0); diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index bb767539c0..0bcff07231 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -17,6 +17,7 @@ import { FileAccess, RemoteAuthorities } from 'vs/base/common/network'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; import { insane, InsaneOptions } from 'vs/base/common/insane/insane'; import { KeyCode } from 'vs/base/common/keyCodes'; +import { withNullAsUndefined } from 'vs/base/common/types'; export function clearNode(node: HTMLElement): void { while (node.firstChild) { @@ -25,79 +26,12 @@ export function clearNode(node: HTMLElement): void { } /** - * @deprecated use `node.remove()` instead + * @deprecated Use node.isConnected directly */ -export function removeNode(node: HTMLElement): void { - if (node.parentNode) { - node.parentNode.removeChild(node); - } -} - -export function trustedInnerHTML(node: Element, value: TrustedHTML): void { - // this is a workaround for innerHTML not allowing for "asymetric" accessors - // see https://github.com/microsoft/vscode/issues/106396#issuecomment-692625393 - // and https://github.com/microsoft/TypeScript/issues/30024 - node.innerHTML = value as unknown as string; -} - export function isInDOM(node: Node | null): boolean { return node?.isConnected ?? false; } -interface IDomClassList { - hasClass(node: HTMLElement | SVGElement, className: string): boolean; - addClass(node: HTMLElement | SVGElement, className: string): void; - addClasses(node: HTMLElement | SVGElement, ...classNames: string[]): void; - removeClass(node: HTMLElement | SVGElement, className: string): void; - removeClasses(node: HTMLElement | SVGElement, ...classNames: string[]): void; - toggleClass(node: HTMLElement | SVGElement, className: string, shouldHaveIt?: boolean): void; -} - -const _classList: IDomClassList = new class implements IDomClassList { - hasClass(node: HTMLElement, className: string): boolean { - return Boolean(className) && node.classList && node.classList.contains(className); - } - - addClasses(node: HTMLElement, ...classNames: string[]): void { - classNames.forEach(nameValue => nameValue.split(' ').forEach(name => this.addClass(node, name))); - } - - addClass(node: HTMLElement, className: string): void { - if (className && node.classList) { - node.classList.add(className); - } - } - - removeClass(node: HTMLElement, className: string): void { - if (className && node.classList) { - node.classList.remove(className); - } - } - - removeClasses(node: HTMLElement, ...classNames: string[]): void { - classNames.forEach(nameValue => nameValue.split(' ').forEach(name => this.removeClass(node, name))); - } - - toggleClass(node: HTMLElement, className: string, shouldHaveIt?: boolean): void { - if (node.classList) { - node.classList.toggle(className, shouldHaveIt); - } - } -}; - -/** @deprecated ES6 - use classList*/ -export function hasClass(node: HTMLElement | SVGElement, className: string): boolean { return _classList.hasClass(node, className); } -/** @deprecated ES6 - use classList*/ -export function addClass(node: HTMLElement | SVGElement, className: string): void { return _classList.addClass(node, className); } -/** @deprecated ES6 - use classList*/ -export function addClasses(node: HTMLElement | SVGElement, ...classNames: string[]): void { return _classList.addClasses(node, ...classNames); } -/** @deprecated ES6 - use classList*/ -export function removeClass(node: HTMLElement | SVGElement, className: string): void { return _classList.removeClass(node, className); } -/** @deprecated ES6 - use classList*/ -export function removeClasses(node: HTMLElement | SVGElement, ...classNames: string[]): void { return _classList.removeClasses(node, ...classNames); } -/** @deprecated ES6 - use classList*/ -export function toggleClass(node: HTMLElement | SVGElement, className: string, shouldHaveIt?: boolean): void { return _classList.toggleClass(node, className, shouldHaveIt); } - class DomListener implements IDisposable { private _handler: (e: any) => void; @@ -414,16 +348,7 @@ export function getClientArea(element: HTMLElement): Dimension { // If visual view port exits and it's on mobile, it should be used instead of window innerWidth / innerHeight, or document.body.clientWidth / document.body.clientHeight if (platform.isIOS && window.visualViewport) { - const width = window.visualViewport.width; - const height = window.visualViewport.height - ( - browser.isStandalone - // in PWA mode, the visual viewport always includes the safe-area-inset-bottom (which is for the home indicator) - // even when you are using the onscreen monitor, the visual viewport will include the area between system statusbar and the onscreen keyboard - // plus the area between onscreen keyboard and the bottom bezel, which is 20px on iOS. - ? (20 + 4) // + 4px for body margin - : 0 - ); - return new Dimension(width, height); + return new Dimension(window.visualViewport.width, window.visualViewport.height); } // Try innerWidth / innerHeight @@ -1254,28 +1179,44 @@ export function computeScreenAwareSize(cssPx: number): number { } /** + * Open safely a new window. This is the best way to do so, but you cannot tell + * if the window was opened or if it was blocked by the brower's popup blocker. + * If you want to tell if the browser blocked the new window, use `windowOpenNoOpenerWithSuccess`. + * * See https://github.com/microsoft/monaco-editor/issues/601 * To protect against malicious code in the linked site, particularly phishing attempts, * the window.opener should be set to null to prevent the linked site from having access * to change the location of the current page. * See https://mathiasbynens.github.io/rel-noopener/ */ -export function windowOpenNoOpener(url: string): boolean { - 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); +export function windowOpenNoOpener(url: string): void { + // By using 'noopener' in the `windowFeatures` argument, the newly created window will + // not be able to use `window.opener` to reach back to the current page. + // See https://stackoverflow.com/a/46958731 + // See https://developer.mozilla.org/en-US/docs/Web/API/Window/open#noopener + // However, this also doesn't allow us to realize if the browser blocked + // the creation of the window. + window.open(url, '_blank', 'noopener'); +} + +/** + * Open safely a new window. This technique is not appropiate in certain contexts, + * like for example when the JS context is executing inside a sandboxed iframe. + * If it is not necessary to know if the browser blocked the new window, use + * `windowOpenNoOpener`. + * + * See https://github.com/microsoft/monaco-editor/issues/601 + * See https://github.com/microsoft/monaco-editor/issues/2474 + * See https://mathiasbynens.github.io/rel-noopener/ + */ +export function windowOpenNoOpenerWithSuccess(url: string): boolean { + const newTab = window.open(); + if (newTab) { + (newTab as any).opener = null; + newTab.location.href = url; return true; - } else { - let newTab = window.open(); - if (newTab) { - (newTab as any).opener = null; - newTab.location.href = url; - return true; - } - return false; } + return false; } export function animate(fn: () => void): IDisposable { @@ -1333,6 +1274,29 @@ export function triggerDownload(dataOrUri: Uint8Array | URI, name: string): void setTimeout(() => document.body.removeChild(anchor)); } +export function triggerUpload(): Promise { + return new Promise(resolve => { + + // In order to upload to the browser, create a + // input element of type `file` and click it + // to gather the selected files + const input = document.createElement('input'); + document.body.appendChild(input); + input.type = 'file'; + input.multiple = true; + + // Resolve once the input event has fired once + Event.once(Event.fromDOMEventEmitter(input, 'input'))(() => { + resolve(withNullAsUndefined(input.files)); + }); + + input.click(); + + // Ensure to remove the element from DOM eventually + setTimeout(() => document.body.removeChild(input)); + }); +} + export enum DetectedFullscreenMode { /** @@ -1518,6 +1482,9 @@ export class ModifierKeyEmitter extends Emitter { }; this._subscriptions.add(domEvent(window, 'keydown', true)(e => { + if (e.defaultPrevented) { + return; + } const event = new StandardKeyboardEvent(e); // If Alt-key keydown event is repeated, ignore it #112347 @@ -1552,6 +1519,10 @@ export class ModifierKeyEmitter extends Emitter { })); this._subscriptions.add(domEvent(window, 'keyup', true)(e => { + if (e.defaultPrevented) { + return; + } + if (!e.altKey && this._keyStatus.altKey) { this._keyStatus.lastKeyReleased = 'alt'; } else if (!e.ctrlKey && this._keyStatus.ctrlKey) { @@ -1642,3 +1613,13 @@ export function getCookieValue(name: string): string | undefined { return match ? match.pop() : undefined; } + +export function addMatchMediaChangeListener(query: string, callback: () => void): void { + const mediaQueryList = window.matchMedia(query); + if (typeof mediaQueryList.addEventListener === 'function') { + mediaQueryList.addEventListener('change', callback); + } else { + // Safari 13.x + mediaQueryList.addListener(callback); + } +} diff --git a/src/vs/base/browser/event.ts b/src/vs/base/browser/event.ts index 160c0bca87..36d073f089 100644 --- a/src/vs/base/browser/event.ts +++ b/src/vs/base/browser/event.ts @@ -3,7 +3,9 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { GestureEvent } from 'vs/base/browser/touch'; import { Event as BaseEvent, Emitter } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; export type EventHandler = HTMLElement | HTMLDocument | Window; @@ -12,6 +14,9 @@ export interface IDomEvent { (element: EventHandler, type: string, useCapture?: boolean): BaseEvent; } +/** + * @deprecated Use `DomEmitter` instead + */ export const domEvent: IDomEvent = (element: EventHandler, type: string, useCapture?: boolean) => { const fn = (e: Event) => emitter.fire(e); const emitter = new Emitter({ @@ -26,6 +31,35 @@ export const domEvent: IDomEvent = (element: EventHandler, type: string, useCapt return emitter.event; }; +export interface DOMEventMap extends HTMLElementEventMap { + '-monaco-gesturetap': GestureEvent; + '-monaco-gesturechange': GestureEvent; + '-monaco-gesturestart': GestureEvent; + '-monaco-gesturesend': GestureEvent; + '-monaco-gesturecontextmenu': GestureEvent; +} + +export class DomEmitter implements IDisposable { + + private emitter: Emitter; + + get event(): BaseEvent { + return this.emitter.event; + } + + constructor(element: EventHandler, type: K, useCapture?: boolean) { + const fn = (e: Event) => this.emitter.fire(e as DOMEventMap[K]); + this.emitter = new Emitter({ + onFirstListenerAdd: () => element.addEventListener(type, fn, useCapture), + onLastListenerRemove: () => element.removeEventListener(type, fn, useCapture) + }); + } + + dispose(): void { + this.emitter.dispose(); + } +} + export interface CancellableEvent { preventDefault(): void; stopPropagation(): void; diff --git a/src/vs/base/browser/globalMouseMoveMonitor.ts b/src/vs/base/browser/globalMouseMoveMonitor.ts index e80c5b9040..444bf31a0d 100644 --- a/src/vs/base/browser/globalMouseMoveMonitor.ts +++ b/src/vs/base/browser/globalMouseMoveMonitor.ts @@ -7,6 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { IframeUtils } from 'vs/base/browser/iframe'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { isIOS } from 'vs/base/common/platform'; export interface IStandardMouseMoveEventData { leftButton: boolean; @@ -88,7 +89,7 @@ export class GlobalMouseMoveMonitor implements I this._onStopCallback = onStopCallback; const windowChain = IframeUtils.getSameOriginWindowChain(); - const mouseMove = 'mousemove'; + const mouseMove = isIOS ? 'pointermove' : 'mousemove'; // Safari sends wrong event, workaround for #122653 const mouseUp = 'mouseup'; const listenTo: (Document | ShadowRoot)[] = windowChain.map(element => element.window.document); diff --git a/src/vs/base/browser/history.ts b/src/vs/base/browser/history.ts index 8222d98952..48fbb8218f 100644 --- a/src/vs/base/browser/history.ts +++ b/src/vs/base/browser/history.ts @@ -9,4 +9,4 @@ export interface IHistoryNavigationWidget { showNextValue(): void; -} \ No newline at end of file +} diff --git a/src/vs/base/browser/iframe.ts b/src/vs/base/browser/iframe.ts index 53741014c3..9d05cea753 100644 --- a/src/vs/base/browser/iframe.ts +++ b/src/vs/base/browser/iframe.ts @@ -43,18 +43,6 @@ function getParentWindowIfSameOrigin(w: Window): Window | null { return w.parent; } -function findIframeElementInParentWindow(parentWindow: Window, childWindow: Window): HTMLIFrameElement | null { - let parentWindowIframes = parentWindow.document.getElementsByTagName('iframe'); - let iframe: HTMLIFrameElement; - for (let i = 0, len = parentWindowIframes.length; i < len; i++) { - iframe = parentWindowIframes[i]; - if (iframe.contentWindow === childWindow) { - return iframe; - } - } - return null; -} - export class IframeUtils { /** @@ -72,7 +60,7 @@ export class IframeUtils { if (parent) { sameOriginWindowChainCache.push({ window: w, - iframeElement: findIframeElementInParentWindow(parent, w) + iframeElement: w.frameElement || null }); } else { sameOriginWindowChainCache.push({ diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 615d13d3dc..4482363979 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as DOM from 'vs/base/browser/dom'; // {{SQL CARBON EDIT}} added missing import to fix build break +import * as DOM from 'vs/base/browser/dom'; import { createElement, FormattedTextRenderOptions } from 'vs/base/browser/formattedTextRenderer'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IMarkdownString, parseHrefAndDimensions, removeMarkdownEscapes } from 'vs/base/common/htmlContent'; @@ -180,15 +180,6 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende }); }); - // const promise = Promise.all([value, withInnerHTML]).then(values => { - // const span = element.querySelector(`div[data-code="${id}"]`); - // if (span) { - // DOM.reset(span, values[0]); - // } - // }).catch(_err => { - // // ignore - // }); - if (options.asyncRenderCallback) { promise.then(options.asyncRenderCallback); } @@ -324,6 +315,14 @@ function getInsaneOptions(options: { readonly isTrusted?: boolean }): InsaneOpti }; } +/** + * Strips all markdown from `string`, if it's an IMarkdownString. For example + * `# Header` would be output as `Header`. If it's not, the string is returned. + */ +export function renderStringAsPlaintext(string: IMarkdownString | string) { + return typeof string === 'string' ? string : renderMarkdownAsPlaintext(string); +} + /** * Strips all markdown from `markdown`. For example `# Header` would be output as `Header`. */ @@ -398,6 +397,7 @@ export function renderMarkdownAsPlaintext(markdown: IMarkdownString) { const unescapeInfo = new Map([ ['"', '"'], + [' ', ' '], ['&', '&'], [''', '\''], ['<', '<'], diff --git a/src/vs/base/browser/ui/actionbar/actionbar.css b/src/vs/base/browser/ui/actionbar/actionbar.css index b6b0985e86..9e217dc69f 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.css +++ b/src/vs/base/browser/ui/actionbar/actionbar.css @@ -75,6 +75,16 @@ margin-right: .8em; } +.monaco-action-bar .action-item .action-label.separator { + width: 1px; + height: 16px; + margin: 5px 4px !important; + cursor: default; + min-width: 1px; + padding: 0; + background-color: #bbb; +} + .secondary-actions .monaco-action-bar .action-label { margin-left: 6px; } diff --git a/src/vs/base/browser/ui/aria/aria.css b/src/vs/base/browser/ui/aria/aria.css index e05407ac1c..0b9eec1012 100644 --- a/src/vs/base/browser/ui/aria/aria.css +++ b/src/vs/base/browser/ui/aria/aria.css @@ -6,4 +6,4 @@ .monaco-aria-container { position: absolute; /* try to hide from window but not from screen readers */ left:-999em; -} \ No newline at end of file +} diff --git a/src/vs/base/browser/ui/button/button.css b/src/vs/base/browser/ui/button/button.css index 00703d4fd7..c01abfbaae 100644 --- a/src/vs/base/browser/ui/button/button.css +++ b/src/vs/base/browser/ui/button/button.css @@ -35,6 +35,7 @@ .monaco-button-dropdown { display: flex; + cursor: pointer; } .monaco-button-dropdown > .monaco-dropdown-button { diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index a9d421b4ed..5b0755af1a 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -185,6 +185,7 @@ export class Button extends Disposable implements IButton { this.buttonSecondaryHoverBackground = styles.buttonSecondaryHoverBackground; this.buttonBorder = styles.buttonBorder; + // {{SQL CARBON EDIT}} this.buttonSecondaryBorder = styles.buttonSecondaryBorder; this.buttonDisabledBackground = styles.buttonDisabledBackground; this.buttonDisabledForeground = styles.buttonDisabledForeground; diff --git a/src/vs/base/browser/ui/centered/centeredViewLayout.ts b/src/vs/base/browser/ui/centered/centeredViewLayout.ts index 22e24a823b..4ae0149e40 100644 --- a/src/vs/base/browser/ui/centered/centeredViewLayout.ts +++ b/src/vs/base/browser/ui/centered/centeredViewLayout.ts @@ -165,6 +165,7 @@ export class CenteredViewLayout implements IDisposable { this.splitView = undefined; this.emptyViews = undefined; this.container.appendChild(this.view.element); + this.view.layout(this.width, this.height, 0, 0); } } diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index 016e9a1e000af380e5512c4bdb85aec40db82477..eec2d491b9c142d8e745468c1acb4ac9fb38a4ae 100644 GIT binary patch delta 3954 zcmXZf2~HHOJ@ zjwj7xHZ|5P7&XQav$@q;O_kUrwVK*AnJ=+6TT`20@8>P&{NA1SX5PFx-(Akf{ypB? zU-aG{<$4_8RsdCX?X_Klj$HZ@@ZAm!uV2x2SL4HT=AQ*3P6L;lni^{B!#94IA)mc0 z((DD_z5#RP^$`)>)V_M{=AfH5<@>Gz1NOFc*42(HU(^qTe+xJ+w%4xha%|-dd4HsQ zer!i=dqd>zu)K{RQse{vU7f2|UmAEU9(3daL+3nnBr2y>-jo}F-q4t9ONV5Che2MS z%N2a`Mv?s`x8lv4H*a`-<@JS}`oUqZ%AefBR`(db!gD=m=|8~T=W{Z-^w!tGs~BFl zUd2lE2fRz+?KMVj8hq;ud_2GOZ1o)XoHQloocjac#D;zN55C8P_?%O4k;CyGMj7499T- zzsG6(5ohryoX4N>7hJ+$B}t#+Z@7xT<1_pN24CVUT*E(c9sk1D_!i&c-}nJH@L&9h zpU{t+1if7xKp*&_ybDW$R@7fR63c%T;?%<7_$+GEPMzTm!bqarHXvq zSNw0dulo;l1=zw09%hfWx&Y5IC1nIpt-?Nu%axQ3JatM62%dT+r36odg4DC8QAv5h zvqIsb#3m&r2T!v+*wZXeeotbH;wX1VKwR5{zCc$>suE5=IPsyIoaT@pd*Lyrs~i<2cjq$o4O)2$dU(XFI=;CWc#5sClD zD0%%T;A0O=>{&E;)&S`)m_bVV3`P=aNyotiD(O9#!AiOiW{8r0gb7m8nJ~dhdK5<1 z*^+LB8LFgjVI*UgbTEvh&}9$O%rGOAv^7kqk_Ly7$!JNd!;DhW^e|yc+8<`LvQyWD zD;WtevbdHE2N+pIOU49Dgu+p{V8+Uckl6tfsbq@4j8igCU}WVinJh5TN@fhqcqP*Y zCPvBJftjFW0>MZ&ESW_xw=0=SFp^<2NlHit6wDn;Mioq~lA#47>9%Bi!Ne&UWH6F$ z3zsD(C>d@rQo5FmIhZ6R0}m!y$>@VgQ8EN!Y%Llg^AILY$z+5{S281EGL%e97->gK z<|a(0k_ifvrDT@EWGk7fFjJMxR~V;~Neh#sTrzZFa+Qowe7$#rINQRlFWH`f2 zS2CtyW+)lhFa=6RH%y_zO^GuV<#(GRMQ@44Hvh%a3%TI&>Tfz~V9LTpg9#n-543vPv? zt&Ju{TN}-aHgzqEk~)`Zl@mdmyEa9eyLLsJyADN3mFZNp>FQFn>H3+XP1j0Ao32%g zHZ7|aZ7P0#iwm^5SfglDai^kP{nPK~C&)Zd9~OwMoft0Omd=`vREzmFy5;exYQq0JB-)HFwLX3MRXI<@mNc zBrKj4?)A-frv9)%0;&7Jf@f<@wlR0niGn75>G1TOFX5R z?cNa{#Sh)R;i03J%C)mf_Gd8X6n6Gp4KJkYKx{~CW9_|9}(3iM5acAPGBu7$5Qg_m|Q~pSsIA%o*pbckXju z%kg*RWaiZ5e3~1RdpK`w-j=){@?-O#%J0kXpH?$%!?bPFmruVm!)L~}8J`wZ7n~}L zF5FPKzwr9Z%9*`IF-6OYwiLZu^ii>+IKOy%@!{gHXC=(qG3(Oo=-CJ6#LRhi&h?Tt zC5K9G%x#?eYH3nwRq5KYEoCRlu9gRtuPlGG{B*^LiqjRY?<+GZH&pJb{A8Z@y!Xy}Y*X^qhs_$xuX=rM=yJ2g?6AdpnhBekS?rglaB5lQ<6_=V)nvOKDYd+KB z(^A~Br{(+BnXMhIeXTc!wPm(-v|VoxY4>zAb_R8JcYfBD(sLwY)cgvmD!{AHs}%vV ziytQc;iX5021f-)21gE$oG^gF>Au0CQ8`Y>wVa%vLO7-WxYO&g{w2Fx&#d>F>~!}3 zig)%0B<+clrJ79wHTF}96CHIb+BWM!|4^r3F%`T zgT3M$PNsAGu8$*}Q}TQ=S{~TCs=z;`aoW)Gd!`PVzwO1H>m%kBQ&pRBB cwu7H}^Rlm>Z*#}0u7(^b delta 3867 zcmXxn33Sxe6$bF{W*4#`TOb4y2tte@`(_d*E7=K22-%S=WM3W%E1j8r5?4` z6%kOYP*6lf1ypJ=+88NPi`FXURhm+vEfv%6aV?zR|K`mMb8_BY&SmpXk3Bm)HitPM z1>_z8RV}?u1D6g2909y}o>JA`b5mRG(Q#2g$N}JVct>l~%HZmg$NAaQl+?i&yoWtY z`7__g+>YMWYi|!98{_wV1$b`n>2GODHkXEgV4mHEdz;n{xZN!m`TjJ1KD4i?w{`aO zL0Pu}WA+2%2Kra6KEJv7UWkkWlX4z75SGye;b-ZR@2QC^0@BZ8y!#oxf>Yay-TxoQ z#>OtXf9L)!&pvRouksK7jdZzY$;&dhdr-!Cx<229mJ(Sk z<+4;NWtmh@$x^cR4UX2lA@NQ=UGSEz5G^BbPp6zc{f?FyeVb|}^oMmiPVVeC@2B}Tdx zQW<-ctYGX_as^|bk}Dbe6)O@W1AuF|r zi#^BJ)+tskMx2WEi;?vT?=!MLEh%tq^ow?8^K_?TZDZsv1^YX0RFc5BNl7r{-Ab-u zaSFz_{{1m$n#$T}yVFDC85{7qcu{U9O=N7vZh81G5UtuOGb}kI_Yq5u6rT|X+ zU^~N1RcvgSK*bh^Vd5<|JIr*&wucE)8~`vg6h{FJFVwO#*YM&jjt7`oih}|sL~&#| zVM2KzP7j#bigN^pmt}FHz%Y{*XA8_+#VG@GmEydCnWs2;V3+`lGYBSJaT>ues}|=H zOoYXv;)sG_>Maf{7^c|b*n(N0IKW_-VvC~;CR%Z*!Ne$zHy9SP#X$!Xr#SLp;uVJ< z49m~r7=&>q@a}{Qx;)I29 zD9&1#4CUn5g~?PLz%W^gqZlSzaVW#&D2`{CT*W~Rvq*7d!{jLrZLhwC0>k+m3T9jD)C_~Q{u~5uEcgsg%W?prAh)AE0vv{W|@*n z%)hBpGKH~P$yCM~C4r2!N^A$zDG6e%S2BySK}iVXawVaR*D0~Zy+VmC;zn}vA-0N} zl-Me6R$?o+MTxD}l}c<;wkolO*rvo5U%L`pCml*yBc@Y{O=Fi5rqOA-c_6WQ>``L# z*sH|mu}_K3UB42h%?v29sr#7{o4V_j*wn33VpFtQiA~E5mpLIeFKd+8yxge7reduU zn~Iy1*i_uC#9sf;mDuaQMTx!ITb0@&U6=aSefaj?1upa3g`)u8_(2yyBJu zvqN!Tfw8|V;VoC>jIf3Kd3s54hk>!zK-^|v>@^Vg8kkoUHyjvyC4?c@Ju{-6$vnNP z#5UV&O6=v^Zzr*x{kq~t1am;)KE^i`?+WIi;=Tm)rsC!Vb4bB{=UYl_kGu`E*@xK1 zct?qCn8Qla8ILGg$aqwVgYjJ@8I12K$z*(ANfzTVC26iFg2Uzy^K@Ksw!wU;xVym& zD!aw){ysQgoM$4_BR54JTrhRPi75Z5^-*V|;yOs`DI;T}OR&K03Q2EueIm^ye&8upt+FspNy|rdu&B~fnwLZ0Z zwa4mw>MH6Ut~*^HQGZMQw)zVV`=@%E|ho7xX{OzwEBv!V0F&ab;7x;AwUcZYYEcJJ*z*%LImr?h9V z=UnfxzP!HC{_FdX4oqfdJ_wm!z5Ci5hZZ2v-DbJP$0a8vJ7oSm z{`H>a=I`b`C(6ws$@9Y!;{&IJNkduRHFLWkt*$R$l8_WUGuV+;J+YuOI3kvsCRc2%cr%mg+ck4~LlH79JhE?-Y9U-6XUi<8~ Y4bru**-eJr_N92rY43fZe$wajKT5GA8~^|S diff --git a/src/vs/base/browser/ui/dialog/dialog.ts b/src/vs/base/browser/ui/dialog/dialog.ts index 75db5131e6..176230b60b 100644 --- a/src/vs/base/browser/ui/dialog/dialog.ts +++ b/src/vs/base/browser/ui/dialog/dialog.ts @@ -87,7 +87,7 @@ export class Dialog extends Disposable { private readonly inputs: InputBox[]; private readonly buttons: string[]; - constructor(private container: HTMLElement, private message: string, buttons: string[], private options: IDialogOptions) { + constructor(private container: HTMLElement, private message: string, buttons: string[] | undefined, private options: IDialogOptions) { super(); this.modalElement = this.container.appendChild($(`.monaco-dialog-modal-block.dimmed`)); @@ -96,21 +96,22 @@ export class Dialog extends Disposable { this.element.setAttribute('role', 'dialog'); hide(this.element); - this.buttons = buttons.length ? buttons : [nls.localize('ok', "OK")]; // If no button is provided, default to OK + this.buttons = Array.isArray(buttons) && buttons.length ? buttons : [nls.localize('ok', "OK")]; // If no button is provided, default to OK const buttonsRowElement = this.element.appendChild($('.dialog-buttons-row')); this.buttonsContainer = buttonsRowElement.appendChild($('.dialog-buttons')); const messageRowElement = this.element.appendChild($('.dialog-message-row')); - this.iconElement = messageRowElement.appendChild($('.dialog-icon')); + this.iconElement = messageRowElement.appendChild($('#monaco-dialog-icon.dialog-icon')); + this.iconElement.setAttribute('aria-label', this.getIconAriaLabel()); this.messageContainer = messageRowElement.appendChild($('.dialog-message-container')); if (this.options.detail || this.options.renderBody) { const messageElement = this.messageContainer.appendChild($('.dialog-message')); - const messageTextElement = messageElement.appendChild($('.dialog-message-text')); + const messageTextElement = messageElement.appendChild($('#monaco-dialog-message-text.dialog-message-text')); messageTextElement.innerText = this.message; } - this.messageDetailElement = this.messageContainer.appendChild($('.dialog-message-detail')); + this.messageDetailElement = this.messageContainer.appendChild($('#monaco-dialog-message-detail.dialog-message-detail')); if (this.options.detail || !this.options.renderBody) { this.messageDetailElement.innerText = this.options.detail ? this.options.detail : message; } else { @@ -118,8 +119,12 @@ export class Dialog extends Disposable { } if (this.options.renderBody) { - const customBody = this.messageContainer.appendChild($('.dialog-message-body')); + const customBody = this.messageContainer.appendChild($('#monaco-dialog-message-body.dialog-message-body')); this.options.renderBody(customBody); + + for (const el of this.messageContainer.querySelectorAll('a')) { + el.tabIndex = 0; + } } if (this.options.inputs) { @@ -157,7 +162,7 @@ export class Dialog extends Disposable { this.toolbarContainer = toolbarRowElement.appendChild($('.dialog-toolbar')); } - private getAriaLabel(): string { + private getIconAriaLabel(): string { let typeLabel = nls.localize('dialogInfoMessage', 'Info'); switch (this.options.type) { case 'error': @@ -176,7 +181,7 @@ export class Dialog extends Disposable { break; } - return `${typeLabel}: ${this.message} ${this.options.detail || ''}`; + return typeLabel; } updateMessage(message: string): void { @@ -218,6 +223,10 @@ export class Dialog extends Disposable { this._register(domEvent(window, 'keydown', true)((e: KeyboardEvent) => { const evt = new StandardKeyboardEvent(e); + if (evt.equals(KeyMod.Alt)) { + evt.preventDefault(); + } + if (evt.equals(KeyCode.Enter)) { // Enter in input field should OK the dialog @@ -246,6 +255,17 @@ export class Dialog extends Disposable { // Build a list of focusable elements in their visual order const focusableElements: { focus: () => void }[] = []; let focusedIndex = -1; + + if (this.messageContainer) { + const links = this.messageContainer.querySelectorAll('a'); + for (const link of links) { + focusableElements.push(link); + if (link === document.activeElement) { + focusedIndex = focusableElements.length - 1; + } + } + } + for (const input of this.inputs) { focusableElements.push(input); if (input.hasFocus()) { @@ -371,7 +391,7 @@ export class Dialog extends Disposable { this.applyStyles(); - this.element.setAttribute('aria-label', this.getAriaLabel()); + this.element.setAttribute('aria-labelledby', 'monaco-dialog-icon monaco-dialog-message-text monaco-dialog-message-detail monaco-dialog-message-body'); show(this.element); // Focus first element (input or button) diff --git a/src/vs/base/browser/ui/dropdown/dropdown.css b/src/vs/base/browser/ui/dropdown/dropdown.css index d33a6d411e..1a14ee7e30 100644 --- a/src/vs/base/browser/ui/dropdown/dropdown.css +++ b/src/vs/base/browser/ui/dropdown/dropdown.css @@ -37,3 +37,10 @@ line-height: 16px; margin-left: -4px; } + +.monaco-dropdown-with-primary > .dropdown-action-container > .monaco-dropdown > .dropdown-label > .action-label { + display: block; + background-size: 16px; + background-position: center center; + background-repeat: no-repeat; +} diff --git a/src/vs/base/browser/ui/dropdown/dropdown.ts b/src/vs/base/browser/ui/dropdown/dropdown.ts index 636da8b5e1..184fc52f95 100644 --- a/src/vs/base/browser/ui/dropdown/dropdown.ts +++ b/src/vs/base/browser/ui/dropdown/dropdown.ts @@ -57,7 +57,7 @@ export class BaseDropdown extends ActionRunner { for (const event of [EventType.MOUSE_DOWN, GestureEventType.Tap]) { this._register(addDisposableListener(this._label, event, e => { if (e instanceof MouseEvent && e.detail > 1) { - return; // prevent multiple clicks to open multiple context menus (https://github.com/Microsoft/vscode/issues/41363) + return; // prevent multiple clicks to open multiple context menus (https://github.com/microsoft/vscode/issues/41363) } if (this.visible) { @@ -71,7 +71,7 @@ export class BaseDropdown extends ActionRunner { this._register(addDisposableListener(this._label, EventType.KEY_UP, e => { const event = new StandardKeyboardEvent(e); if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { - EventHelper.stop(e, true); // https://github.com/Microsoft/vscode/issues/57997 + EventHelper.stop(e, true); // https://github.com/microsoft/vscode/issues/57997 if (this.visible) { this.hide(); diff --git a/src/vs/base/browser/ui/findinput/findInput.css b/src/vs/base/browser/ui/findinput/findInput.css index 0361d01703..aede088028 100644 --- a/src/vs/base/browser/ui/findinput/findInput.css +++ b/src/vs/base/browser/ui/findinput/findInput.css @@ -62,4 +62,4 @@ 0% { background: rgba(255, 255, 255, 0.44); } /* Made intentionally different such that the CSS minifier does not collapse the two animations into a single one*/ 99% { background: transparent; } -} \ No newline at end of file +} diff --git a/src/vs/base/browser/ui/findinput/findInput.ts b/src/vs/base/browser/ui/findinput/findInput.ts index ea709ee25d..fac449f169 100644 --- a/src/vs/base/browser/ui/findinput/findInput.ts +++ b/src/vs/base/browser/ui/findinput/findInput.ts @@ -128,7 +128,7 @@ export class FindInput extends Widget { const flexibleMaxHeight = options.flexibleMaxHeight; this.domNode = document.createElement('div'); - dom.addClass(this.domNode, 'monaco-findInput'); + this.domNode.classList.add('monaco-findInput'); this.inputBox = this._register(new HistoryInputBox(this.domNode, this.contextViewProvider, { placeholder: this.placeholder || '', @@ -263,7 +263,7 @@ export class FindInput extends Widget { } public enable(): void { - dom.removeClass(this.domNode, 'disabled'); + this.domNode.classList.remove('disabled'); this.inputBox.enable(); this.regex.enable(); this.wholeWords.enable(); @@ -271,7 +271,7 @@ export class FindInput extends Widget { } public disable(): void { - dom.addClass(this.domNode, 'disabled'); + this.domNode.classList.add('disabled'); this.inputBox.disable(); this.regex.disable(); this.wholeWords.disable(); @@ -403,9 +403,9 @@ export class FindInput extends Widget { private _lastHighlightFindOptions: number = 0; public highlightFindOptions(): void { - dom.removeClass(this.domNode, 'highlight-' + (this._lastHighlightFindOptions)); + this.domNode.classList.remove('highlight-' + (this._lastHighlightFindOptions)); this._lastHighlightFindOptions = 1 - this._lastHighlightFindOptions; - dom.addClass(this.domNode, 'highlight-' + (this._lastHighlightFindOptions)); + this.domNode.classList.add('highlight-' + (this._lastHighlightFindOptions)); } public validate(): void { diff --git a/src/vs/base/browser/ui/findinput/replaceInput.ts b/src/vs/base/browser/ui/findinput/replaceInput.ts index a7c6a888bc..c2b63b2423 100644 --- a/src/vs/base/browser/ui/findinput/replaceInput.ts +++ b/src/vs/base/browser/ui/findinput/replaceInput.ts @@ -136,7 +136,7 @@ export class ReplaceInput extends Widget { const flexibleMaxHeight = options.flexibleMaxHeight; this.domNode = document.createElement('div'); - dom.addClass(this.domNode, 'monaco-findInput'); + this.domNode.classList.add('monaco-findInput'); this.inputBox = this._register(new HistoryInputBox(this.domNode, this.contextViewProvider, { ariaLabel: this.label || '', @@ -234,13 +234,13 @@ export class ReplaceInput extends Widget { } public enable(): void { - dom.removeClass(this.domNode, 'disabled'); + this.domNode.classList.remove('disabled'); this.inputBox.enable(); this.preserveCase.enable(); } public disable(): void { - dom.addClass(this.domNode, 'disabled'); + this.domNode.classList.add('disabled'); this.inputBox.disable(); this.preserveCase.disable(); } @@ -347,9 +347,9 @@ export class ReplaceInput extends Widget { private _lastHighlightFindOptions: number = 0; public highlightFindOptions(): void { - dom.removeClass(this.domNode, 'highlight-' + (this._lastHighlightFindOptions)); + this.domNode.classList.remove('highlight-' + (this._lastHighlightFindOptions)); this._lastHighlightFindOptions = 1 - this._lastHighlightFindOptions; - dom.addClass(this.domNode, 'highlight-' + (this._lastHighlightFindOptions)); + this.domNode.classList.add('highlight-' + (this._lastHighlightFindOptions)); } public validate(): void { diff --git a/src/vs/base/browser/ui/grid/grid.ts b/src/vs/base/browser/ui/grid/grid.ts index da8479a248..771e753a61 100644 --- a/src/vs/base/browser/ui/grid/grid.ts +++ b/src/vs/base/browser/ui/grid/grid.ts @@ -210,7 +210,9 @@ export class Grid extends Disposable { get minimumHeight(): number { return this.gridview.minimumHeight; } get maximumWidth(): number { return this.gridview.maximumWidth; } get maximumHeight(): number { return this.gridview.maximumHeight; } - get onDidChange(): Event<{ width: number; height: number; } | undefined> { return this.gridview.onDidChange; } + + readonly onDidChange: Event<{ width: number; height: number; } | undefined>; + readonly onDidScroll: Event; get boundarySashes(): IBoundarySashes { return this.gridview.boundarySashes; } set boundarySashes(boundarySashes: IBoundarySashes) { this.gridview.boundarySashes = boundarySashes; } @@ -232,8 +234,8 @@ export class Grid extends Disposable { } else { this.gridview = new GridView(options); } - this._register(this.gridview); + this._register(this.gridview); this._register(this.gridview.onDidSashReset(this.onDidSashReset, this)); const size: number | GridViewSizing = typeof options.firstViewVisibleCachedSize === 'number' @@ -243,6 +245,9 @@ export class Grid extends Disposable { if (!(view instanceof GridView)) { this._addView(view, size, [0]); } + + this.onDidChange = this.gridview.onDidChange; + this.onDidScroll = this.gridview.onDidScroll; } style(styles: IGridStyles): void { diff --git a/src/vs/base/browser/ui/grid/gridview.css b/src/vs/base/browser/ui/grid/gridview.css index 5c55d8f2bd..257039720b 100644 --- a/src/vs/base/browser/ui/grid/gridview.css +++ b/src/vs/base/browser/ui/grid/gridview.css @@ -13,4 +13,4 @@ .monaco-grid-branch-node { width: 100%; height: 100%; -} \ No newline at end of file +} diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index 08ed7bdadf..659a4a8715 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -9,9 +9,10 @@ import { Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; import { SplitView, IView as ISplitView, Sizing, LayoutPriority, ISplitViewStyles } from 'vs/base/browser/ui/splitview/splitview'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { $ } from 'vs/base/browser/dom'; -import { tail2 as tail } from 'vs/base/common/arrays'; +import { equals as arrayEquals, tail2 as tail } from 'vs/base/common/arrays'; import { Color } from 'vs/base/common/color'; import { clamp } from 'vs/base/common/numbers'; +import { isUndefined } from 'vs/base/common/types'; export { Sizing, LayoutPriority } from 'vs/base/browser/ui/splitview/splitview'; export { Orientation } from 'vs/base/browser/ui/sash/sash'; @@ -242,6 +243,10 @@ class BranchNode implements ISplitView, IDisposable { private readonly _onDidChange = new Emitter(); readonly onDidChange: Event = this._onDidChange.event; + private _onDidScroll = new Emitter(); + private onDidScrollDisposable: IDisposable = Disposable.None; + readonly onDidScroll: Event = this._onDidScroll.event; + private childrenChangeDisposable: IDisposable = Disposable.None; private readonly _onDidSashReset = new Emitter(); @@ -343,11 +348,7 @@ class BranchNode implements ISplitView, IDisposable { const onDidSashReset = Event.map(this.splitview.onDidSashReset, i => [i]); this.splitviewSashResetDisposable = onDidSashReset(this._onDidSashReset.fire, this._onDidSashReset); - const onDidChildrenChange = Event.map(Event.any(...this.children.map(c => c.onDidChange)), () => undefined); - this.childrenChangeDisposable = onDidChildrenChange(this._onDidChange.fire, this._onDidChange); - - const onDidChildrenSashReset = Event.any(...this.children.map((c, i) => Event.map(c.onDidSashReset, location => [i, ...location]))); - this.childrenSashResetDisposable = onDidChildrenSashReset(this._onDidSashReset.fire, this._onDidSashReset); + this.updateChildrenEvents(); } style(styles: IGridViewStyles): void { @@ -566,6 +567,11 @@ class BranchNode implements ISplitView, IDisposable { } private onDidChildrenChange(): void { + this.updateChildrenEvents(); + this._onDidChange.fire(undefined); + } + + private updateChildrenEvents(): void { const onDidChildrenChange = Event.map(Event.any(...this.children.map(c => c.onDidChange)), () => undefined); this.childrenChangeDisposable.dispose(); this.childrenChangeDisposable = onDidChildrenChange(this._onDidChange.fire, this._onDidChange); @@ -574,7 +580,9 @@ class BranchNode implements ISplitView, IDisposable { this.childrenSashResetDisposable.dispose(); this.childrenSashResetDisposable = onDidChildrenSashReset(this._onDidSashReset.fire, this._onDidSashReset); - this._onDidChange.fire(undefined); + const onDidScroll = Event.any(Event.signal(this.splitview.onDidScroll), ...this.children.map(c => c.onDidScroll)); + this.onDidScrollDisposable.dispose(); + this.onDidScrollDisposable = onDidScroll(this._onDidScroll.fire, this._onDidScroll); } trySet2x2(other: BranchNode): IDisposable { @@ -646,6 +654,25 @@ class BranchNode implements ISplitView, IDisposable { } } +/** + * Creates a latched event that avoids being fired when the view + * constraints do not change at all. + */ +function createLatchedOnDidChangeViewEvent(view: IView): Event { + const [onDidChangeViewConstraints, onDidSetViewSize] = Event.split(view.onDidChange, isUndefined); + + return Event.any( + onDidSetViewSize, + Event.map( + Event.latch( + Event.map(onDidChangeViewConstraints, _ => ([view.minimumWidth, view.maximumWidth, view.minimumHeight, view.maximumHeight])), + arrayEquals + ), + _ => undefined + ) + ); +} + class LeafNode implements ISplitView, IDisposable { private _size: number = 0; @@ -657,6 +684,7 @@ class LeafNode implements ISplitView, IDisposable { private absoluteOffset: number = 0; private absoluteOrthogonalOffset: number = 0; + readonly onDidScroll: Event = Event.None; readonly onDidSashReset: Event = Event.None; private _onDidLinkedWidthNodeChange = new Relay(); @@ -691,7 +719,8 @@ class LeafNode implements ISplitView, IDisposable { this._orthogonalSize = orthogonalSize; this._size = size; - this._onDidViewChange = Event.map(this.view.onDidChange, e => e && (this.orientation === Orientation.VERTICAL ? e.width : e.height)); + const onDidChange = createLatchedOnDidChangeViewEvent(view); + this._onDidViewChange = Event.map(onDidChange, e => e && (this.orientation === Orientation.VERTICAL ? e.width : e.height)); this.onDidChange = Event.any(this._onDidViewChange, this._onDidSetLinkedNode.event, this._onDidLinkedWidthNodeChange.event, this._onDidLinkedHeightNodeChange.event); } @@ -778,7 +807,25 @@ class LeafNode implements ISplitView, IDisposable { this._orthogonalSize = ctx.orthogonalSize; this.absoluteOffset = ctx.absoluteOffset + offset; this.absoluteOrthogonalOffset = ctx.absoluteOrthogonalOffset; - this.view.layout(this.width, this.height, this.top, this.left); + + this._layout(this.width, this.height, this.top, this.left); + } + + private cachedWidth: number = 0; + private cachedHeight: number = 0; + private cachedTop: number = 0; + private cachedLeft: number = 0; + + private _layout(width: number, height: number, top: number, left: number): void { + if (this.cachedWidth === width && this.cachedHeight === height && this.cachedTop === top && this.cachedLeft === left) { + return; + } + + this.cachedWidth = width; + this.cachedHeight = height; + this.cachedTop = top; + this.cachedLeft = left; + this.view.layout(width, height, top, left); } setVisible(visible: boolean): void { @@ -852,6 +899,7 @@ export class GridView implements IDisposable { this.element.appendChild(root.element); this.onDidSashResetRelay.input = root.onDidSashReset; this._onDidChange.input = Event.map(root.onDidChange, () => undefined); // TODO + this._onDidScroll.input = root.onDidScroll; } get orientation(): Orientation { @@ -877,6 +925,9 @@ export class GridView implements IDisposable { get maximumWidth(): number { return this.root.maximumHeight; } get maximumHeight(): number { return this.root.maximumHeight; } + private _onDidScroll = new Relay(); + readonly onDidScroll = this._onDidScroll.event; + private _onDidChange = new Relay(); readonly onDidChange = this._onDidChange.event; diff --git a/src/vs/base/browser/ui/hover/hover.css b/src/vs/base/browser/ui/hover/hover.css index 04bb299e82..69a28789ff 100644 --- a/src/vs/base/browser/ui/hover/hover.css +++ b/src/vs/base/browser/ui/hover/hover.css @@ -138,3 +138,8 @@ margin-bottom: 4px; display: inline-block; } + +.monaco-hover-content .action-container a { + -webkit-user-select: none; + user-select: none; +} diff --git a/src/vs/base/browser/ui/iconLabel/iconHoverDelegate.ts b/src/vs/base/browser/ui/iconLabel/iconHoverDelegate.ts index c5ab792d2c..222dc942c6 100644 --- a/src/vs/base/browser/ui/iconLabel/iconHoverDelegate.ts +++ b/src/vs/base/browser/ui/iconLabel/iconHoverDelegate.ts @@ -16,9 +16,11 @@ export interface IHoverDelegateOptions { text: IMarkdownString | string; target: IHoverDelegateTarget | HTMLElement; hoverPosition?: HoverPosition; + showPointer?: boolean; } export interface IHoverDelegate { showHover(options: IHoverDelegateOptions): IDisposable | undefined; delay: number; + placement?: 'mouse' | 'element'; } diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index c060b300ab..6b81479c67 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -10,13 +10,10 @@ import { IMatch } from 'vs/base/common/filters'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { Range } from 'vs/base/common/range'; import { equals } from 'vs/base/common/objects'; -import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; import { IMarkdownString } from 'vs/base/common/htmlContent'; -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'; -import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { setupCustomHover, setupNativeHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; export interface IIconLabelCreationOptions { supportHighlights?: boolean; @@ -91,17 +88,17 @@ class FastLabelNode { export class IconLabel extends Disposable { - private domNode: FastLabelNode; + private readonly domNode: FastLabelNode; - private nameNode: Label | LabelWithHighlights; + private readonly nameNode: Label | LabelWithHighlights; - private descriptionContainer: FastLabelNode; + private readonly descriptionContainer: FastLabelNode; private descriptionNode: FastLabelNode | HighlightedLabel | undefined; - private descriptionNodeFactory: () => FastLabelNode | HighlightedLabel; + private readonly descriptionNodeFactory: () => FastLabelNode | HighlightedLabel; - private labelContainer: HTMLElement; + private readonly labelContainer: HTMLElement; - private hoverDelegate: IHoverDelegate | undefined = undefined; + private readonly hoverDelegate: IHoverDelegate | undefined; private readonly customHovers: Map = new Map(); constructor(container: HTMLElement, options?: IIconLabelCreationOptions) { @@ -114,7 +111,7 @@ export class IconLabel extends Disposable { const nameContainer = dom.append(this.labelContainer, dom.$('span.monaco-icon-name-container')); this.descriptionContainer = this._register(new FastLabelNode(dom.append(this.labelContainer, dom.$('span.monaco-icon-description-container')))); - if (options?.supportHighlights) { + if (options?.supportHighlights || options?.supportIcons) { this.nameNode = new LabelWithHighlights(nameContainer, !!options.supportIcons); } else { this.nameNode = new Label(nameContainer); @@ -126,9 +123,7 @@ export class IconLabel extends Disposable { this.descriptionNodeFactory = () => this._register(new FastLabelNode(dom.append(this.descriptionContainer.element, dom.$('span.label-description')))); } - if (options?.hoverDelegate) { - this.hoverDelegate = options.hoverDelegate; - } + this.hoverDelegate = options?.hoverDelegate; } get element(): HTMLElement { @@ -185,116 +180,21 @@ export class IconLabel extends Disposable { } if (!this.hoverDelegate) { - return this.setupNativeHover(htmlElement, tooltip); + setupNativeHover(htmlElement, tooltip); } else { - return this.setupCustomHover(this.hoverDelegate, htmlElement, tooltip); + const hoverDisposable = setupCustomHover(this.hoverDelegate, htmlElement, tooltip); + if (hoverDisposable) { + this.customHovers.set(htmlElement, hoverDisposable); + } } } - 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); + public override dispose() { + super.dispose(); + for (const disposable of this.customHovers.values()) { + disposable.dispose(); } - 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 = this.getTooltipForCustom(markdownTooltip); - - let hoverOptions: IHoverDelegateOptions | undefined; - let mouseX: number | undefined; - let isHovering = false; - let tokenSource: CancellationTokenSource; - let hoverDisposable: IDisposable | undefined; - function mouseOver(this: HTMLElement, e: MouseEvent): void { - if (isHovering) { - return; - } - tokenSource = new CancellationTokenSource(); - function mouseLeaveOrDown(this: HTMLElement, e: MouseEvent): void { - const isMouseDown = e.type === dom.EventType.MOUSE_DOWN; - if (isMouseDown) { - hoverDisposable?.dispose(); - hoverDisposable = undefined; - } - if (isMouseDown || (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): void { - mouseX = e.x; - } - const mouseMoveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_MOVE, true)(mouseMove.bind(htmlElement)); - setTimeout(async () => { - if (isHovering && tooltip) { - // Re-use the already computed hover options if they exist. - if (!hoverOptions) { - const target: IHoverDelegateTarget = { - targetElements: [this], - dispose: () => { } - }; - hoverOptions = { - text: localize('iconLabel.loading', "Loading..."), - target, - hoverPosition: HoverPosition.BELOW - }; - hoverDisposable = IconLabel.adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering); - - const resolvedTooltip = (await tooltip(tokenSource.token)) ?? (!isString(markdownTooltip) ? markdownTooltip.markdownNotSupportedFallback : undefined); - if (resolvedTooltip) { - hoverOptions = { - text: resolvedTooltip, - target, - hoverPosition: HoverPosition.BELOW - }; - // awaiting the tooltip could take a while. Make sure we're still hovering. - hoverDisposable = IconLabel.adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering); - } else if (hoverDisposable) { - hoverDisposable.dispose(); - hoverDisposable = undefined; - } - } - - } - mouseMoveDisposable.dispose(); - }, hoverDelegate.delay); - } - const mouseOverDisposable = this._register(domEvent(htmlElement, dom.EventType.MOUSE_OVER, true)(mouseOver.bind(htmlElement))); - this.customHovers.set(htmlElement, mouseOverDisposable); - } - - private setupNativeHover(htmlElement: HTMLElement, tooltip: string | IIconLabelMarkdownString | undefined): void { - let stringTooltip: string = ''; - if (isString(tooltip)) { - stringTooltip = tooltip; - } else if (tooltip?.markdownNotSupportedFallback) { - stringTooltip = tooltip.markdownNotSupportedFallback; - } - htmlElement.title = stringTooltip; + this.customHovers.clear(); } } diff --git a/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts b/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts new file mode 100644 index 0000000000..24d0803660 --- /dev/null +++ b/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { isFunction, isString } from 'vs/base/common/types'; +import * as dom from 'vs/base/browser/dom'; +import { IIconLabelMarkdownString } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { DomEmitter } from 'vs/base/browser/event'; +import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; +import { localize } from 'vs/nls'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; + + +export function setupNativeHover(htmlElement: HTMLElement, tooltip: string | IIconLabelMarkdownString | undefined): void { + if (isString(tooltip)) { + htmlElement.title = tooltip; + } else if (tooltip?.markdownNotSupportedFallback) { + htmlElement.title = tooltip.markdownNotSupportedFallback; + } else { + htmlElement.removeAttribute('title'); + } +} + +export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString | undefined): IDisposable | undefined { + if (!markdownTooltip) { + return undefined; + } + + const tooltip = getTooltipForCustom(markdownTooltip); + + let hoverOptions: IHoverDelegateOptions | undefined; + let mouseX: number | undefined; + let isHovering = false; + let tokenSource: CancellationTokenSource; + let hoverDisposable: IDisposable | undefined; + + const mouseOverDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_OVER, true); + mouseOverDomEmitter.event((e: MouseEvent) => { + if (isHovering) { + return; + } + tokenSource = new CancellationTokenSource(); + function mouseLeaveOrDown(e: MouseEvent): void { + const isMouseDown = e.type === dom.EventType.MOUSE_DOWN; + if (isMouseDown) { + hoverDisposable?.dispose(); + hoverDisposable = undefined; + } + if (isMouseDown || (e).fromElement === htmlElement) { + isHovering = false; + hoverOptions = undefined; + tokenSource.dispose(true); + mouseLeaveDomEmitter.dispose(); + mouseDownDomEmitter.dispose(); + } + } + const mouseLeaveDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_LEAVE, true); + mouseLeaveDomEmitter.event(mouseLeaveOrDown); + const mouseDownDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_DOWN, true); + mouseDownDomEmitter.event(mouseLeaveOrDown); + isHovering = true; + + function mouseMove(e: MouseEvent): void { + mouseX = e.x; + } + const mouseMoveDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_MOVE, true); + mouseMoveDomEmitter.event(mouseMove); + setTimeout(async () => { + if (isHovering && tooltip) { + // Re-use the already computed hover options if they exist. + if (!hoverOptions) { + const target: IHoverDelegateTarget = { + targetElements: [htmlElement], + dispose: () => { } + }; + hoverOptions = { + text: localize('iconLabel.loading', "Loading..."), + target, + hoverPosition: HoverPosition.BELOW + }; + hoverDisposable = adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering); + + const resolvedTooltip = (await tooltip(tokenSource.token)) ?? (!isString(markdownTooltip) ? markdownTooltip.markdownNotSupportedFallback : undefined); + if (resolvedTooltip) { + hoverOptions = { + text: resolvedTooltip, + target, + showPointer: hoverDelegate.placement === 'element', + hoverPosition: HoverPosition.BELOW + }; + // awaiting the tooltip could take a while. Make sure we're still hovering. + hoverDisposable = adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering); + } else if (hoverDisposable) { + hoverDisposable.dispose(); + hoverDisposable = undefined; + } + } + + } + mouseMoveDomEmitter.dispose(); + }, hoverDelegate.delay); + }); + return mouseOverDomEmitter; +} + + +function 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; + } +} + +function adjustXAndShowCustomHover(hoverOptions: IHoverDelegateOptions | undefined, mouseX: number | undefined, hoverDelegate: IHoverDelegate, isHovering: boolean): IDisposable | undefined { + if (hoverOptions && isHovering) { + if (mouseX !== undefined && (hoverDelegate.placement === undefined || hoverDelegate.placement === 'mouse')) { + (hoverOptions.target).x = mouseX + 10; + } + return hoverDelegate.showHover(hoverOptions); + } + return undefined; +} diff --git a/src/vs/base/browser/ui/iconLabel/iconlabel.css b/src/vs/base/browser/ui/iconLabel/iconlabel.css index abac1d6638..9e7370ecff 100644 --- a/src/vs/base/browser/ui/iconLabel/iconlabel.css +++ b/src/vs/base/browser/ui/iconLabel/iconlabel.css @@ -28,7 +28,7 @@ -moz-osx-font-smoothing: grayscale; vertical-align: top; - flex-shrink: 0; /* fix for https://github.com/Microsoft/vscode/issues/13787 */ + flex-shrink: 0; /* fix for https://github.com/microsoft/vscode/issues/13787 */ } .monaco-icon-label > .monaco-icon-label-container { diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 1df3888afc..b2dd132e09 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -898,7 +898,7 @@ export class ListView implements ISpliceable, IDisposable { @memoize get onMouseOver(): Event> { return Event.map(domEvent(this.domNode, 'mouseover'), e => this.toMouseEvent(e)); } @memoize get onMouseMove(): Event> { return Event.map(domEvent(this.domNode, 'mousemove'), e => this.toMouseEvent(e)); } @memoize get onMouseOut(): Event> { return Event.map(domEvent(this.domNode, 'mouseout'), e => this.toMouseEvent(e)); } - @memoize get onContextMenu(): Event> { return Event.map(domEvent(this.domNode, 'contextmenu'), e => this.toMouseEvent(e)); } + @memoize get onContextMenu(): Event | IListGestureEvent> { return Event.any(Event.map(domEvent(this.domNode, 'contextmenu'), e => this.toMouseEvent(e)), Event.map(domEvent(this.domNode, TouchEventType.Contextmenu) as Event, e => this.toGestureEvent(e))); } @memoize get onTouchStart(): Event> { return Event.map(domEvent(this.domNode, 'touchstart'), e => this.toTouchEvent(e)); } @memoize get onTap(): Event> { return Event.map(domEvent(this.rowsContainer, TouchEventType.Tap), e => this.toGestureEvent(e as GestureEvent)); } diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index a6b9b83488..fe3eea4424 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -660,9 +660,15 @@ export class MouseController implements IDisposable { private changeSelection(e: IListMouseEvent | IListTouchEvent): void { const focus = e.index!; - const anchor = this.list.getAnchor(); + let anchor = this.list.getAnchor(); + + if (this.isSelectionRangeChangeEvent(e)) { + if (typeof anchor === 'undefined') { + const currentFocus = this.list.getFocus()[0]; + anchor = currentFocus ?? focus; + this.list.setAnchor(anchor); + } - if (this.isSelectionRangeChangeEvent(e) && typeof anchor === 'number') { const min = Math.min(anchor, focus); const max = Math.max(anchor, focus); const rangeSelection = range(min, max + 1); @@ -1201,7 +1207,7 @@ export class List implements ISpliceable, IThemable, IDisposable { const fromMouse = Event.chain(this.view.onContextMenu) .filter(_ => !didJustPressContextMenuKey) - .map(({ element, index, browserEvent }) => ({ element, index, anchor: { x: browserEvent.clientX + 1, y: browserEvent.clientY }, browserEvent })) + .map(({ element, index, browserEvent }) => ({ element, index, anchor: { x: browserEvent.pageX + 1, y: browserEvent.pageY }, browserEvent })) .event; return Event.any>(fromKeyDown, fromKeyUp, fromMouse); diff --git a/src/vs/base/browser/ui/list/splice.ts b/src/vs/base/browser/ui/list/splice.ts index 559b94b2c4..6b5cd4e368 100644 --- a/src/vs/base/browser/ui/list/splice.ts +++ b/src/vs/base/browser/ui/list/splice.ts @@ -16,4 +16,4 @@ export class CombinedSpliceable implements ISpliceable { splice(start: number, deleteCount: number, elements: T[]): void { this.spliceables.forEach(s => s.splice(start, deleteCount, elements)); } -} \ No newline at end of file +} diff --git a/src/vs/base/browser/ui/sash/sash.ts b/src/vs/base/browser/ui/sash/sash.ts index 097d4fd21e..54f6a7f1a6 100644 --- a/src/vs/base/browser/ui/sash/sash.ts +++ b/src/vs/base/browser/ui/sash/sash.ts @@ -4,15 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./sash'; -import { IDisposable, dispose, Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { isMacintosh } from 'vs/base/common/platform'; -import * as types from 'vs/base/common/types'; -import { EventType, GestureEvent, Gesture } from 'vs/base/browser/touch'; -import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { EventType, Gesture, GestureEvent } from 'vs/base/browser/touch'; import { Event, Emitter } from 'vs/base/common/event'; -import { getElementsByTagName, EventHelper, createStyleSheet, addDisposableListener, append, $ } from 'vs/base/browser/dom'; -import { domEvent } from 'vs/base/browser/event'; +import { getElementsByTagName, EventHelper, createStyleSheet, append, $, EventLike } from 'vs/base/browser/dom'; +import { DomEmitter } from 'vs/base/browser/event'; import { Delayer } from 'vs/base/common/async'; +import { memoize } from 'vs/base/common/decorators'; let DEBUG = false; // DEBUG = Boolean("true"); // done "weirdly" so that a lint warning prevents you from pushing this @@ -88,6 +87,78 @@ export function setGlobalHoverDelay(size: number): void { onDidChangeHoverDelay.fire(size); } +interface PointerEvent extends EventLike { + readonly pageX: number; + readonly pageY: number; + readonly altKey: boolean; + readonly target: EventTarget | null; +} + +interface IPointerEventFactory { + readonly onPointerMove: Event; + readonly onPointerUp: Event; + dispose(): void; +} + +class MouseEventFactory implements IPointerEventFactory { + + private disposables = new DisposableStore(); + + @memoize + get onPointerMove(): Event { + return this.disposables.add(new DomEmitter(window, 'mousemove')).event; + } + + @memoize + get onPointerUp(): Event { + return this.disposables.add(new DomEmitter(window, 'mouseup')).event; + } + + dispose(): void { + this.disposables.dispose(); + } +} + +class GestureEventFactory implements IPointerEventFactory { + + private disposables = new DisposableStore(); + + @memoize + get onPointerMove(): Event { + return this.disposables.add(new DomEmitter(this.el, EventType.Change)).event; + } + + @memoize + get onPointerUp(): Event { + return this.disposables.add(new DomEmitter(this.el, EventType.End)).event; + } + + constructor(private el: HTMLElement) { } + + dispose(): void { + this.disposables.dispose(); + } +} + +class OrthogonalPointerEventFactory implements IPointerEventFactory { + + @memoize + get onPointerMove(): Event { + return this.factory.onPointerMove; + } + + @memoize + get onPointerUp(): Event { + return this.factory.onPointerUp; + } + + constructor(private factory: IPointerEventFactory) { } + + dispose(): void { + // noop + } +} + export class Sash extends Disposable { private el: HTMLElement; @@ -146,9 +217,9 @@ export class Sash extends Disposable { if (state !== SashState.Disabled) { this._orthogonalStartDragHandle = append(this.el, $('.orthogonal-drag-handle.start')); this.orthogonalStartDragHandleDisposables.add(toDisposable(() => this._orthogonalStartDragHandle!.remove())); - domEvent(this._orthogonalStartDragHandle, 'mouseenter') + this.orthogonalEndDragHandleDisposables.add(new DomEmitter(this._orthogonalStartDragHandle, 'mouseenter')).event (() => Sash.onMouseEnter(sash), undefined, this.orthogonalStartDragHandleDisposables); - domEvent(this._orthogonalStartDragHandle, 'mouseleave') + this.orthogonalEndDragHandleDisposables.add(new DomEmitter(this._orthogonalStartDragHandle, 'mouseleave')).event (() => Sash.onMouseLeave(sash), undefined, this.orthogonalStartDragHandleDisposables); } }; @@ -176,9 +247,9 @@ export class Sash extends Disposable { if (state !== SashState.Disabled) { this._orthogonalEndDragHandle = append(this.el, $('.orthogonal-drag-handle.end')); this.orthogonalEndDragHandleDisposables.add(toDisposable(() => this._orthogonalEndDragHandle!.remove())); - domEvent(this._orthogonalEndDragHandle, 'mouseenter') + this.orthogonalEndDragHandleDisposables.add(new DomEmitter(this._orthogonalEndDragHandle, 'mouseenter')).event (() => Sash.onMouseEnter(sash), undefined, this.orthogonalEndDragHandleDisposables); - domEvent(this._orthogonalEndDragHandle, 'mouseleave') + this.orthogonalEndDragHandleDisposables.add(new DomEmitter(this._orthogonalEndDragHandle, 'mouseleave')).event (() => Sash.onMouseLeave(sash), undefined, this.orthogonalEndDragHandleDisposables); } }; @@ -205,13 +276,28 @@ export class Sash extends Disposable { this.el.classList.add('mac'); } - this._register(domEvent(this.el, 'mousedown')(this.onMouseDown, this)); - this._register(domEvent(this.el, 'dblclick')(this.onMouseDoubleClick, this)); - this._register(domEvent(this.el, 'mouseenter')(() => Sash.onMouseEnter(this))); - this._register(domEvent(this.el, 'mouseleave')(() => Sash.onMouseLeave(this))); + const onMouseDown = this._register(new DomEmitter(this.el, 'mousedown')).event; + this._register(onMouseDown(e => this.onPointerStart(e, new MouseEventFactory()), this)); + const onMouseDoubleClick = this._register(new DomEmitter(this.el, 'dblclick')).event; + this._register(onMouseDoubleClick(this.onPointerDoublePress, this)); + const onMouseEnter = this._register(new DomEmitter(this.el, 'mouseenter')).event; + this._register(onMouseEnter(() => Sash.onMouseEnter(this))); + const onMouseLeave = this._register(new DomEmitter(this.el, 'mouseleave')).event; + this._register(onMouseLeave(() => Sash.onMouseLeave(this))); this._register(Gesture.addTarget(this.el)); - this._register(domEvent(this.el, EventType.Start)(e => this.onTouchStart(e as GestureEvent), this)); + + const onTouchStart = Event.map(this._register(new DomEmitter(this.el, EventType.Start)).event, e => ({ ...e, target: e.initialTarget ?? null })); + this._register(onTouchStart(e => this.onPointerStart(e, new GestureEventFactory(this.el)), this)); + const onTap = this._register(new DomEmitter(this.el, EventType.Tap)).event; + const onDoubleTap = Event.map( + Event.filter( + Event.debounce(onTap, (res, event) => ({ event, count: (res?.count ?? 0) + 1 }), 250), + ({ count }) => count === 2 + ), + ({ event }) => ({ ...event, target: event.initialTarget ?? null }) + ); + this._register(onDoubleTap(this.onPointerDoublePress, this)); if (typeof options.size === 'number') { this.size = options.size; @@ -252,24 +338,24 @@ export class Sash extends Disposable { this.layout(); } - private onMouseDown(e: MouseEvent): void { - EventHelper.stop(e, false); + private onPointerStart(event: PointerEvent, pointerEventFactory: IPointerEventFactory): void { + EventHelper.stop(event); let isMultisashResize = false; - if (!(e as any).__orthogonalSashEvent) { - const orthogonalSash = this.getOrthogonalSash(e); + if (!(event as any).__orthogonalSashEvent) { + const orthogonalSash = this.getOrthogonalSash(event); if (orthogonalSash) { isMultisashResize = true; - (e as any).__orthogonalSashEvent = true; - orthogonalSash.onMouseDown(e); + (event as any).__orthogonalSashEvent = true; + orthogonalSash.onPointerStart(event, new OrthogonalPointerEventFactory(pointerEventFactory)); } } - if (this.linkedSash && !(e as any).__linkedSashEvent) { - (e as any).__linkedSashEvent = true; - this.linkedSash.onMouseDown(e); + if (this.linkedSash && !(event as any).__linkedSashEvent) { + (event as any).__linkedSashEvent = true; + this.linkedSash.onPointerStart(event, new OrthogonalPointerEventFactory(pointerEventFactory)); } if (!this.state) { @@ -287,10 +373,9 @@ export class Sash extends Disposable { iframe.style.pointerEvents = 'none'; // disable mouse events on iframes as long as we drag the sash } - const mouseDownEvent = new StandardMouseEvent(e); - const startX = mouseDownEvent.posx; - const startY = mouseDownEvent.posy; - const altKey = mouseDownEvent.altKey; + const startX = event.pageX; + const startY = event.pageY; + const altKey = event.altKey; const startEvent: ISashEvent = { startX, currentX: startX, startY, currentY: startY, altKey }; this.el.classList.add('active'); @@ -332,15 +417,14 @@ export class Sash extends Disposable { this.onDidEnablementChange(updateStyle, null, disposables); } - const onMouseMove = (e: MouseEvent) => { + const onPointerMove = (e: PointerEvent) => { EventHelper.stop(e, false); - const mouseMoveEvent = new StandardMouseEvent(e); - const event: ISashEvent = { startX, currentX: mouseMoveEvent.posx, startY, currentY: mouseMoveEvent.posy, altKey }; + const event: ISashEvent = { startX, currentX: e.pageX, startY, currentY: e.pageY, altKey }; this._onDidChange.fire(event); }; - const onMouseUp = (e: MouseEvent) => { + const onPointerUp = (e: PointerEvent) => { EventHelper.stop(e, false); this.el.removeChild(style); @@ -355,11 +439,12 @@ export class Sash extends Disposable { } }; - domEvent(window, 'mousemove')(onMouseMove, null, disposables); - domEvent(window, 'mouseup')(onMouseUp, null, disposables); + pointerEventFactory.onPointerMove(onPointerMove, null, disposables); + pointerEventFactory.onPointerUp(onPointerUp, null, disposables); + disposables.add(pointerEventFactory); } - private onMouseDoubleClick(e: MouseEvent): void { + private onPointerDoublePress(e: MouseEvent): void { const orthogonalSash = this.getOrthogonalSash(e); if (orthogonalSash) { @@ -373,41 +458,6 @@ export class Sash extends Disposable { this._onDidReset.fire(); } - private onTouchStart(event: GestureEvent): void { - EventHelper.stop(event); - - const listeners: IDisposable[] = []; - - const startX = event.pageX; - const startY = event.pageY; - const altKey = event.altKey; - - this._onDidStart.fire({ - startX: startX, - currentX: startX, - startY: startY, - currentY: startY, - altKey - }); - - listeners.push(addDisposableListener(this.el, EventType.Change, (event: GestureEvent) => { - if (types.isNumber(event.pageX) && types.isNumber(event.pageY)) { - this._onDidChange.fire({ - startX: startX, - currentX: event.pageX, - startY: startY, - currentY: event.pageY, - altKey - }); - } - })); - - listeners.push(addDisposableListener(this.el, EventType.End, () => { - this._onDidEnd.fire(); - dispose(listeners); - })); - } - private static onMouseEnter(sash: Sash, fromLinkedSash: boolean = false): void { if (sash.el.classList.contains('active')) { sash.hoverDelayer.cancel(); @@ -476,7 +526,7 @@ export class Sash extends Disposable { return this.hidden; } - private getOrthogonalSash(e: MouseEvent): Sash | undefined { + private getOrthogonalSash(e: PointerEvent): Sash | undefined { if (!e.target || !(e.target instanceof HTMLElement)) { return undefined; } diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts index 9a4d8b8ae7..b8fcfb106f 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts @@ -330,7 +330,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi } if (this.styles.listFocusForeground) { - content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused:not(:hover) { color: ${this.styles.listFocusForeground} !important; }`); + content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused { color: ${this.styles.listFocusForeground} !important; }`); } if (this.styles.decoratorRightForeground) { diff --git a/src/vs/base/browser/ui/splitview/paneview.ts b/src/vs/base/browser/ui/splitview/paneview.ts index 5e14b2f5b2..b9b080d6ca 100644 --- a/src/vs/base/browser/ui/splitview/paneview.ts +++ b/src/vs/base/browser/ui/splitview/paneview.ts @@ -16,6 +16,7 @@ import { isFirefox } from 'vs/base/browser/browser'; import { DataTransfers } from 'vs/base/browser/dnd'; import { Orientation } from 'vs/base/browser/ui/sash/sash'; import { localize } from 'vs/nls'; +import { ScrollEvent } from 'vs/base/common/scrollable'; export interface IPaneOptions { minimumBodySize?: number; @@ -444,6 +445,7 @@ export class PaneView extends Disposable { orientation: Orientation; readonly onDidSashChange: Event; + readonly onDidScroll: Event; constructor(container: HTMLElement, options: IPaneViewOptions = {}) { super(); @@ -453,6 +455,7 @@ export class PaneView extends Disposable { this.element = append(container, $('.monaco-pane-view')); this.splitview = this._register(new SplitView(this.element, { orientation: this.orientation })); this.onDidSashChange = this.splitview.onDidSashChange; + this.onDidScroll = this.splitview.onDidScroll; } addPane(pane: Pane, size: number, index = this.splitview.length): void { diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index ab8a53f4e0..aebe1069a2 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -14,7 +14,7 @@ import { Color } from 'vs/base/common/color'; import { domEvent } from 'vs/base/browser/event'; import { $, append, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; import { SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; -import { Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { Scrollable, ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable'; export { Orientation } from 'vs/base/browser/ui/sash/sash'; export interface ISplitViewStyles { @@ -237,6 +237,8 @@ export class SplitView extends Disposable { private _onDidSashReset = this._register(new Emitter()); readonly onDidSashReset = this._onDidSashReset.event; + readonly onDidScroll: Event; + get length(): number { return this.viewItems.length; } @@ -317,7 +319,8 @@ export class SplitView extends Disposable { horizontal: this.orientation === Orientation.HORIZONTAL ? (options.scrollbarVisibility ?? ScrollbarVisibility.Auto) : ScrollbarVisibility.Hidden }, this.scrollable)); - this._register(this.scrollableElement.onScroll(e => { + this.onDidScroll = this.scrollableElement.onScroll; + this._register(this.onDidScroll(e => { this.viewContainer.scrollTop = e.scrollTop; this.viewContainer.scrollLeft = e.scrollLeft; })); diff --git a/src/vs/base/browser/ui/table/table.css b/src/vs/base/browser/ui/table/table.css index 89edc6f0ff..615eca2061 100644 --- a/src/vs/base/browser/ui/table/table.css +++ b/src/vs/base/browser/ui/table/table.css @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ .monaco-table { diff --git a/src/vs/base/browser/ui/table/table.ts b/src/vs/base/browser/ui/table/table.ts index 2a9786bf42..da6b1a599a 100644 --- a/src/vs/base/browser/ui/table/table.ts +++ b/src/vs/base/browser/ui/table/table.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IListContextMenuEvent, IListEvent, IListGestureEvent, IListMouseEvent, IListRenderer, IListTouchEvent } from 'vs/base/browser/ui/list/list'; diff --git a/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts index 2600fcb750..7914022e22 100644 --- a/src/vs/base/browser/ui/table/tableWidget.ts +++ b/src/vs/base/browser/ui/table/tableWidget.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import 'vs/css!./table'; diff --git a/src/vs/base/browser/ui/tree/indexTreeModel.ts b/src/vs/base/browser/ui/tree/indexTreeModel.ts index 12f900f933..bd4a5f3850 100644 --- a/src/vs/base/browser/ui/tree/indexTreeModel.ts +++ b/src/vs/base/browser/ui/tree/indexTreeModel.ts @@ -5,7 +5,7 @@ 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 { 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'; @@ -256,7 +256,7 @@ export class IndexTreeModel, TFilterData = voi } } - const deletedNodes = splice(parentNode.children, lastIndex, deleteCount, nodesToInsert); + const deletedNodes = parentNode.children.splice(lastIndex, deleteCount, ...nodesToInsert); // figure out what is the count of deleted visible children let deletedVisibleChildrenCount = 0; diff --git a/src/vs/base/common/actions.ts b/src/vs/base/common/actions.ts index 74a71ecc4e..a27af5e9f0 100644 --- a/src/vs/base/common/actions.ts +++ b/src/vs/base/common/actions.ts @@ -31,14 +31,14 @@ export interface IAction extends IDisposable { enabled: boolean; checked: boolean; expanded: boolean | undefined; // {{SQL CARBON EDIT}} - run(event?: unknown): Promise; + run(event?: unknown): Promise; // {{SQL CARBON EDIT}} Add promise } export interface IActionRunner extends IDisposable { readonly onDidRun: Event; readonly onBeforeRun: Event; - run(action: IAction, context?: unknown): Promise; + run(action: IAction, context?: unknown): Promise; // {{SQL CARBON EDIT}} Add promise } export interface IActionChangeEvent { @@ -62,9 +62,9 @@ export class Action extends Disposable implements IAction { protected _enabled: boolean = true; protected _checked: boolean = false; protected _expanded: boolean = false; // {{SQL CARBON EDIT}} - protected readonly _actionCallback?: (event?: unknown) => Promise; + protected readonly _actionCallback?: (event?: unknown) => Promise; // {{SQL CARBON EDIT}} Add promise - constructor(id: string, label: string = '', cssClass: string = '', enabled: boolean = true, actionCallback?: (event?: unknown) => Promise) { + constructor(id: string, label: string = '', cssClass: string = '', enabled: boolean = true, actionCallback?: (event?: unknown) => Promise) { // {{SQL CARBON EDIT}} Add promise super(); this._id = id; this._label = label; @@ -212,6 +212,24 @@ export class ActionRunner extends Disposable implements IActionRunner { export class Separator extends Action { + /** + * Joins all non-empty lists of actions with separators. + */ + public static join(...actionLists: readonly IAction[][]) { + let out: IAction[] = []; + for (const list of actionLists) { + if (!list.length) { + // skip + } else if (out.length) { + out = [...out, new Separator(), ...list]; + } else { + out = list; + } + } + + return out; + } + static readonly ID = 'vs.actions.separator'; constructor(label?: string) { @@ -257,6 +275,7 @@ export class SubmenuAction implements IAction { } protected _setExpanded(value: boolean): void { } + // {{SQL CARBON EDIT}} - End } export class EmptySubmenuAction extends Action { diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index 8fe2bb74e2..32f41400b4 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -385,16 +385,6 @@ export function lastIndex(array: ReadonlyArray, fn: (item: T) => boolean): return -1; } -/** - * @deprecated ES6: use `Array.find` - */ -export function first(array: ReadonlyArray, fn: (item: T) => boolean, notFoundValue: T): T; -export function first(array: ReadonlyArray, fn: (item: T) => boolean): T | undefined; -export function first(array: ReadonlyArray, fn: (item: T) => boolean, notFoundValue: T | undefined = undefined): T | undefined { - const index = array.findIndex(fn); - return index < 0 ? notFoundValue : array[index]; -} - export function firstOrDefault(array: ReadonlyArray, notFoundValue: NotFound): T | NotFound; export function firstOrDefault(array: ReadonlyArray): T | undefined; export function firstOrDefault(array: ReadonlyArray, notFoundValue?: NotFound): T | NotFound | undefined { @@ -566,76 +556,35 @@ export function mapFind(array: Iterable, mapFn: (value: T) => R | undef } /** - * An alternative for Array.push(...items) method, Use this if you need to push large number of items to the array. - * Array.push(...item) can only support limited number of items due to the maximum call stack size limit. - * @param array The array to add items to. - * @param items The new items to be added. + * Like Math.min with a delegate, and returns the winning index */ -export function push(array: T[], items: T[]): void { - // set array length and then assign value at index is faster than doing array.push(item) individually - const newLength = array.length + items.length; - const startIdx = array.length; - array.length = newLength; - items.forEach((item, index) => { - array[startIdx + index] = item; - }); -} - -/** - * Insert the new items to array, the insertion be performed on the original array directly, the alternative insertArray() method will return a new array. - * @param array The original array. - * @param start The zero-based location in the array from which to start inserting elements. - * @param newItems The items to be inserted - */ -export function insertArray2(array: T[], start: number, newItems: T[]): void { - const startIdx = getActualStartIndex(array, start); - const originalLength = array.length; - const newItemsLength = newItems.length; - array.length = originalLength + newItemsLength; - // Move the items after the start index, start from the end so that we don't overwrite any value. - for (let i = originalLength - 1; i >= startIdx; i--) { - array[i + newItemsLength] = array[i]; - } - - for (let i = 0; i < newItemsLength; i++) { - array[i + startIdx] = newItems[i]; - } -} - -/** - * Removes elements from an array and inserts new elements in their place, returning the deleted elements. Alternative to the native Array.splice method, it - * can only support limited number of items due to the maximum call stack size limit. - * @param array The original array. - * @param start The zero-based location in the array from which to start removing elements. - * @param deleteCount The number of elements to remove. - * @returns An array containing the elements that were deleted. - */ -export function splice(array: T[], start: number, deleteCount: number, newItems: T[]): T[] { - const startIdx = getActualStartIndex(array, start); - const deletedItems = array.splice(startIdx, deleteCount); - insertArray2(array, startIdx, newItems); - return deletedItems; -} - -/** - * Determine the actual start index (same logic as the native splice() or slice()) - * If greater than the length of the array, start will be set to the length of the array. In this case, no element will be deleted but the method will behave as an adding function, adding as many element as item[n*] provided. - * If negative, it will begin that many elements from the end of the array. (In this case, the origin -1, meaning -n is the index of the nth last element, and is therefore equivalent to the index of array.length - n.) If array.length + start is less than 0, it will begin from index 0. - * @param array The target array. - * @param start The operation index. - */ -function getActualStartIndex(array: T[], start: number): number { - let startIndex: number; - if (start > array.length) { - startIndex = array.length; - } else if (start < 0) { - if ((start + array.length) < 0) { - startIndex = 0; - } else { - startIndex = start + array.length; +export function minIndex(array: readonly T[], fn: (value: T) => number): number { + let minValue = Number.MAX_SAFE_INTEGER; + let minIdx = 0; + array.forEach((value, i) => { + const thisValue = fn(value); + if (thisValue < minValue) { + minValue = thisValue; + minIdx = i; } - } else { - startIndex = start; - } - return startIndex; + }); + + return minIdx; +} + +/** + * Like Math.max with a delegate, and returns the winning index + */ +export function maxIndex(array: readonly T[], fn: (value: T) => number): number { + let minValue = Number.MIN_SAFE_INTEGER; + let maxIdx = 0; + array.forEach((value, i) => { + const thisValue = fn(value); + if (thisValue > minValue) { + minValue = thisValue; + maxIdx = i; + } + }); + + return maxIdx; } diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index ac002178d5..2f11d3e0c5 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -536,7 +536,6 @@ export class Limiter { get size(): number { return this._size; - // return this.runningPromises + this.outstandingPromises.length; } queue(factory: ITask>): Promise { diff --git a/src/vs/base/common/codicons.ts b/src/vs/base/common/codicons.ts index d3df1d2d70..80ab5aa4a5 100644 --- a/src/vs/base/common/codicons.ts +++ b/src/vs/base/common/codicons.ts @@ -3,10 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SqlIconId } from 'sql/base/common/codicons'; -import { codiconStartMarker } from 'vs/base/common/codicon'; +import { SqlIconId } from 'sql/base/common/codicons'; // {{SQL CARBON EDIT}} import { Emitter, Event } from 'vs/base/common/event'; -import { localize } from 'vs/nls'; export interface IIconRegistry { readonly all: IterableIterator; @@ -48,8 +46,8 @@ const _registry = new Registry(); export const iconRegistry: IIconRegistry = _registry; -export function registerCodicon(id: string, def: Codicon, description?: string): Codicon { - return new Codicon(id, def, description); +export function registerCodicon(id: string, def: Codicon): Codicon { + return new Codicon(id, def); } // Selects all codicon names encapsulated in the `$()` syntax and wraps the @@ -114,7 +112,6 @@ export namespace CSSIcon { } return classNames; } - } export function asClassName(icon: CSSIcon): string { @@ -586,36 +583,8 @@ export namespace Codicon { export const filterFilled = new Codicon('filter-filled', { fontCharacter: '\\ebce' }); export const wand = new Codicon('wand', { fontCharacter: '\\ebcf' }); export const debugLineByLine = new Codicon('debug-line-by-line', { fontCharacter: '\\ebd0' }); + export const inspect = new Codicon('inspect', { fontCharacter: '\\ebd1' }); - export const dropDownButton = new Codicon('drop-down-button', Codicon.chevronDown.definition, localize('dropDownButton', 'Icon for drop down buttons.')); + export const dropDownButton = new Codicon('drop-down-button', Codicon.chevronDown.definition); } -// common icons - - - - -const escapeCodiconsRegex = /(\\)?\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)/gi; -export function escapeCodicons(text: string): string { - return text.replace(escapeCodiconsRegex, (match, escaped) => escaped ? match : `\\${match}`); -} - -const markdownEscapedCodiconsRegex = /\\\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)/gi; -export function markdownEscapeEscapedCodicons(text: string): string { - // Need to add an extra \ for escaping in markdown - return text.replace(markdownEscapedCodiconsRegex, match => `\\${match}`); -} - -const markdownUnescapeCodiconsRegex = /(\\)?\$\\\(([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?)\\\)/gi; -export function markdownUnescapeCodicons(text: string): string { - return text.replace(markdownUnescapeCodiconsRegex, (match, escaped, codicon) => escaped ? match : `$(${codicon})`); -} - -const stripCodiconsRegex = /(\s)?(\\)?\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)(\s)?/gi; -export function stripCodicons(text: string): string { - if (text.indexOf(codiconStartMarker) === -1) { - return text; - } - - return text.replace(stripCodiconsRegex, (match, preWhitespace, escaped, postWhitespace) => escaped ? match : preWhitespace || postWhitespace || ''); -} diff --git a/src/vs/base/common/collections.ts b/src/vs/base/common/collections.ts index a54e418bd5..2c0bdbde0b 100644 --- a/src/vs/base/common/collections.ts +++ b/src/vs/base/common/collections.ts @@ -55,8 +55,8 @@ export function forEach(from: IStringDictionary | INumberDictionary, ca * Groups the collection into a dictionary based on the provided * group function. */ -export function groupBy(data: T[], groupFn: (element: T) => string): IStringDictionary { - const result: IStringDictionary = Object.create(null); +export function groupBy(data: V[], groupFn: (element: V) => K): Record { + const result: Record = Object.create(null); for (const element of data) { const key = groupFn(element); let target = result[key]; @@ -68,24 +68,6 @@ export function groupBy(data: T[], groupFn: (element: T) => string): IStringD return result; } -/** - * Groups the collection into a dictionary based on the provided - * group function. - */ -export function groupByNumber(data: T[], groupFn: (element: T) => number): Map { - const result = new Map(); - for (const element of data) { - const key = groupFn(element); - let target = result.get(key); - if (!target) { - target = []; - result.set(key, target); - } - target.push(element); - } - return result; -} - export function fromMap(original: Map): IStringDictionary { const result: IStringDictionary = Object.create(null); if (original) { diff --git a/src/vs/base/common/color.ts b/src/vs/base/common/color.ts index cb580a95cc..b6f9a14724 100644 --- a/src/vs/base/common/color.ts +++ b/src/vs/base/common/color.ts @@ -523,7 +523,7 @@ export namespace Color { /** * The default format will use HEX if opaque and RGBA otherwise. */ - export function format(color: Color): string | null { + export function format(color: Color): string { if (color.isOpaque()) { return Color.Format.CSS.formatHex(color); } diff --git a/src/vs/base/common/comparers.ts b/src/vs/base/common/comparers.ts index 511d920f4d..15705bc051 100644 --- a/src/vs/base/common/comparers.ts +++ b/src/vs/base/common/comparers.ts @@ -6,11 +6,11 @@ import { sep } from 'vs/base/common/path'; import { IdleValue } from 'vs/base/common/async'; -// When comparing large numbers of strings, such as in sorting large arrays, is better for -// performance to create an Intl.Collator object and use the function provided by its compare -// property than it is to use String.prototype.localeCompare() +// When comparing large numbers of strings it's better for performance to create an +// Intl.Collator object and use the function provided by its compare property +// than it is to use String.prototype.localeCompare() -// A collator with numeric sorting enabled, and no sensitivity to case or to accents +// A collator with numeric sorting enabled, and no sensitivity to case, accents or diacritics. const intlFileNameCollatorBaseNumeric: IdleValue<{ collator: Intl.Collator, collatorIsNumeric: boolean }> = new IdleValue(() => { const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }); return { @@ -28,19 +28,20 @@ const intlFileNameCollatorNumeric: IdleValue<{ collator: Intl.Collator }> = new }); // A collator with numeric sorting enabled, and sensitivity to accents and diacritics but not case. -const intlFileNameCollatorNumericCaseInsenstive: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => { +const intlFileNameCollatorNumericCaseInsensitive: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => { const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'accent' }); return { collator: collator }; -});/** Compares filenames without distinguishing the name from the extension. Disambiguates by unicode comparison. */ +}); + +/** Compares filenames without distinguishing the name from the extension. Disambiguates by unicode comparison. */ export function compareFileNames(one: string | null, other: string | null, caseSensitive = false): number { const a = one || ''; const b = other || ''; const result = intlFileNameCollatorBaseNumeric.value.collator.compare(a, b); - // Using the numeric option in the collator will - // make compare(`foo1`, `foo01`) === 0. We must disambiguate. + // Using the numeric option will make compare(`foo1`, `foo01`) === 0. Disambiguate. if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && result === 0 && a !== b) { return a < b ? -1 : 1; } @@ -48,16 +49,45 @@ export function compareFileNames(one: string | null, other: string | null, caseS return result; } -/** Compares filenames without distinguishing the name from the extension. Disambiguates by length, not unicode comparison. */ +/** Compares full filenames without grouping by case. */ export function compareFileNamesDefault(one: string | null, other: string | null): number { const collatorNumeric = intlFileNameCollatorNumeric.value.collator; one = one || ''; other = other || ''; - // Compare the entire filename - both name and extension - and disambiguate by length if needed return compareAndDisambiguateByLength(collatorNumeric, one, other); } +/** Compares full filenames grouping uppercase names before lowercase. */ +export function compareFileNamesUpper(one: string | null, other: string | null) { + const collatorNumeric = intlFileNameCollatorNumeric.value.collator; + one = one || ''; + other = other || ''; + + return compareCaseUpperFirst(one, other) || compareAndDisambiguateByLength(collatorNumeric, one, other); +} + +/** Compares full filenames grouping lowercase names before uppercase. */ +export function compareFileNamesLower(one: string | null, other: string | null) { + const collatorNumeric = intlFileNameCollatorNumeric.value.collator; + one = one || ''; + other = other || ''; + + return compareCaseLowerFirst(one, other) || compareAndDisambiguateByLength(collatorNumeric, one, other); +} + +/** Compares full filenames by unicode value. */ +export function compareFileNamesUnicode(one: string | null, other: string | null) { + one = one || ''; + other = other || ''; + + if (one === other) { + return 0; + } + + return one < other ? -1 : 1; +} + export function noIntlCompareFileNames(one: string | null, other: string | null, caseSensitive = false): number { if (!caseSensitive) { one = one && one.toLowerCase(); @@ -78,6 +108,7 @@ export function noIntlCompareFileNames(one: string | null, other: string | null, return oneExtension < otherExtension ? -1 : 1; } +/** Compares filenames by extension, then by name. Disambiguates by unicode comparison. */ export function compareFileExtensions(one: string | null, other: string | null): number { const [oneName, oneExtension] = extractNameAndExtension(one); const [otherName, otherExtension] = extractNameAndExtension(other); @@ -85,8 +116,7 @@ export function compareFileExtensions(one: string | null, other: string | null): let result = intlFileNameCollatorBaseNumeric.value.collator.compare(oneExtension, otherExtension); if (result === 0) { - // Using the numeric option in the collator will - // make compare(`foo1`, `foo01`) === 0. We must disambiguate. + // Using the numeric option will make compare(`foo1`, `foo01`) === 0. Disambiguate. if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && oneExtension !== otherExtension) { return oneExtension < otherExtension ? -1 : 1; } @@ -102,24 +132,65 @@ export function compareFileExtensions(one: string | null, other: string | null): return result; } -/** Compares filenames by extenson, then by full filename */ +/** Compares filenames by extenson, then by full filename. Mixes uppercase and lowercase names together. */ export function compareFileExtensionsDefault(one: string | null, other: string | null): number { one = one || ''; other = other || ''; const oneExtension = extractExtension(one); const otherExtension = extractExtension(other); const collatorNumeric = intlFileNameCollatorNumeric.value.collator; - const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.value.collator; - let result; + const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsensitive.value.collator; - // Check for extension differences, ignoring differences in case and comparing numbers numerically. - result = compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension); - if (result !== 0) { - return result; + return compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension) || + compareAndDisambiguateByLength(collatorNumeric, one, other); +} + +/** Compares filenames by extension, then case, then full filename. Groups uppercase names before lowercase. */ +export function compareFileExtensionsUpper(one: string | null, other: string | null): number { + one = one || ''; + other = other || ''; + const oneExtension = extractExtension(one); + const otherExtension = extractExtension(other); + const collatorNumeric = intlFileNameCollatorNumeric.value.collator; + const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsensitive.value.collator; + + return compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension) || + compareCaseUpperFirst(one, other) || + compareAndDisambiguateByLength(collatorNumeric, one, other); +} + +/** Compares filenames by extension, then case, then full filename. Groups lowercase names before uppercase. */ +export function compareFileExtensionsLower(one: string | null, other: string | null): number { + one = one || ''; + other = other || ''; + const oneExtension = extractExtension(one); + const otherExtension = extractExtension(other); + const collatorNumeric = intlFileNameCollatorNumeric.value.collator; + const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsensitive.value.collator; + + return compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension) || + compareCaseLowerFirst(one, other) || + compareAndDisambiguateByLength(collatorNumeric, one, other); +} + +/** Compares filenames by case-insensitive extension unicode value, then by full filename unicode value. */ +export function compareFileExtensionsUnicode(one: string | null, other: string | null) { + one = one || ''; + other = other || ''; + const oneExtension = extractExtension(one).toLowerCase(); + const otherExtension = extractExtension(other).toLowerCase(); + + // Check for extension differences + if (oneExtension !== otherExtension) { + return oneExtension < otherExtension ? -1 : 1; } - // Compare full filenames - return compareAndDisambiguateByLength(collatorNumeric, one, other); + // Check for full filename differences. + if (one !== other) { + return one < other ? -1 : 1; + } + + return 0; } const FileNameMatch = /^(.*?)(\.([^.]*))?$/; @@ -130,7 +201,7 @@ function extractNameAndExtension(str?: string | null, dotfilesAsNames = false): let result: [string, string] = [(match && match[1]) || '', (match && match[3]) || '']; - // if the dotfilesAsNames option is selected, treat an empty filename with an extension, + // if the dotfilesAsNames option is selected, treat an empty filename with an extension // or a filename that starts with a dot, as a dotfile name if (dotfilesAsNames && (!result[0] && result[1] || result[0] && result[0].charAt(0) === '.')) { result = [result[0] + '.' + result[1], '']; @@ -162,6 +233,54 @@ function compareAndDisambiguateByLength(collator: Intl.Collator, one: string, ot return 0; } +/** @returns `true` if the string is starts with a lowercase letter. Otherwise, `false`. */ +function startsWithLower(string: string) { + const character = string.charAt(0); + + return (character.toLocaleUpperCase() !== character) ? true : false; +} + +/** @returns `true` if the string starts with an uppercase letter. Otherwise, `false`. */ +function startsWithUpper(string: string) { + const character = string.charAt(0); + + return (character.toLocaleLowerCase() !== character) ? true : false; +} + +/** + * Compares the case of the provided strings - lowercase before uppercase + * + * @returns + * ```text + * -1 if one is lowercase and other is uppercase + * 1 if one is uppercase and other is lowercase + * 0 otherwise + * ``` + */ +function compareCaseLowerFirst(one: string, other: string): number { + if (startsWithLower(one) && startsWithUpper(other)) { + return -1; + } + return (startsWithUpper(one) && startsWithLower(other)) ? 1 : 0; +} + +/** + * Compares the case of the provided strings - uppercase before lowercase + * + * @returns + * ```text + * -1 if one is uppercase and other is lowercase + * 1 if one is lowercase and other is uppercase + * 0 otherwise + * ``` + */ +function compareCaseUpperFirst(one: string, other: string): number { + if (startsWithUpper(one) && startsWithLower(other)) { + return -1; + } + return (startsWithLower(one) && startsWithUpper(other)) ? 1 : 0; +} + function comparePathComponents(one: string, other: string, caseSensitive = false): number { if (!caseSensitive) { one = one && one.toLowerCase(); diff --git a/src/vs/base/common/decorators.ts b/src/vs/base/common/decorators.ts index 8c513d2d48..af629c0b28 100644 --- a/src/vs/base/common/decorators.ts +++ b/src/vs/base/common/decorators.ts @@ -24,64 +24,39 @@ export function createDecorator(mapFn: (fn: Function, key: string) => Function): }; } -let memoizeId = 0; -export function createMemoizer() { - const memoizeKeyPrefix = `$memoize${memoizeId++}`; - let self: any = undefined; +export function memoize(_target: any, key: string, descriptor: any) { + let fnKey: string | null = null; + let fn: Function | null = null; - const result = function memoize(target: any, key: string, descriptor: any) { - let fnKey: string | null = null; - let fn: Function | null = null; + if (typeof descriptor.value === 'function') { + fnKey = 'value'; + fn = descriptor.value; - if (typeof descriptor.value === 'function') { - fnKey = 'value'; - fn = descriptor.value; + if (fn!.length !== 0) { + console.warn('Memoize should only be used in functions with zero parameters'); + } + } else if (typeof descriptor.get === 'function') { + fnKey = 'get'; + fn = descriptor.get; + } - if (fn!.length !== 0) { - console.warn('Memoize should only be used in functions with zero parameters'); - } - } else if (typeof descriptor.get === 'function') { - fnKey = 'get'; - fn = descriptor.get; + if (!fn) { + throw new Error('not supported'); + } + + const memoizeKey = `$memoize$${key}`; + descriptor[fnKey!] = function (...args: any[]) { + if (!this.hasOwnProperty(memoizeKey)) { + Object.defineProperty(this, memoizeKey, { + configurable: false, + enumerable: false, + writable: false, + value: fn!.apply(this, args) + }); } - if (!fn) { - throw new Error('not supported'); - } - - const memoizeKey = `${memoizeKeyPrefix}:${key}`; - descriptor[fnKey!] = function (...args: any[]) { - self = this; - - if (!this.hasOwnProperty(memoizeKey)) { - Object.defineProperty(this, memoizeKey, { - configurable: true, - enumerable: false, - writable: true, - value: fn!.apply(this, args) - }); - } - - return this[memoizeKey]; - }; + return this[memoizeKey]; }; - - result.clear = () => { - if (typeof self === 'undefined') { - return; - } - Object.getOwnPropertyNames(self).forEach(property => { - if (property.indexOf(memoizeKeyPrefix) === 0) { - delete self[property]; - } - }); - }; - - return result; -} - -export function memoize(target: any, key: string, descriptor: any) { - return createMemoizer()(target, key, descriptor); } export interface IDebounceReducer { diff --git a/src/vs/base/common/diff/diff.ts b/src/vs/base/common/diff/diff.ts index 9e63dc4f25..39d641619e 100644 --- a/src/vs/base/common/diff/diff.ts +++ b/src/vs/base/common/diff/diff.ts @@ -27,6 +27,7 @@ export function stringDiff(original: string, modified: string, pretty: boolean): export interface ISequence { getElements(): Int32Array | number[] | string[]; + getStrictElement?(index: number): string; } export interface IDiffChange { @@ -231,6 +232,8 @@ export class LcsDiff { private readonly ContinueProcessingPredicate: IContinueProcessingPredicate | null; + private readonly _originalSequence: ISequence; + private readonly _modifiedSequence: ISequence; private readonly _hasStrings: boolean; private readonly _originalStringElements: string[]; private readonly _originalElementsOrHash: Int32Array; @@ -246,6 +249,9 @@ export class LcsDiff { constructor(originalSequence: ISequence, modifiedSequence: ISequence, continueProcessingPredicate: IContinueProcessingPredicate | null = null) { this.ContinueProcessingPredicate = continueProcessingPredicate; + this._originalSequence = originalSequence; + this._modifiedSequence = modifiedSequence; + const [originalStringElements, originalElementsOrHash, originalHasStrings] = LcsDiff._getElements(originalSequence); const [modifiedStringElements, modifiedElementsOrHash, modifiedHasStrings] = LcsDiff._getElements(modifiedSequence); @@ -288,6 +294,22 @@ export class LcsDiff { return (this._hasStrings ? this._originalStringElements[originalIndex] === this._modifiedStringElements[newIndex] : true); } + private ElementsAreStrictEqual(originalIndex: number, newIndex: number): boolean { + if (!this.ElementsAreEqual(originalIndex, newIndex)) { + return false; + } + const originalElement = LcsDiff._getStrictElement(this._originalSequence, originalIndex); + const modifiedElement = LcsDiff._getStrictElement(this._modifiedSequence, newIndex); + return (originalElement === modifiedElement); + } + + private static _getStrictElement(sequence: ISequence, index: number): string | null { + if (typeof sequence.getStrictElement === 'function') { + return sequence.getStrictElement(index); + } + return null; + } + private OriginalElementsAreEqual(index1: number, index2: number): boolean { if (this._originalElementsOrHash[index1] !== this._originalElementsOrHash[index2]) { return false; @@ -813,10 +835,18 @@ export class LcsDiff { const checkOriginal = change.originalLength > 0; const checkModified = change.modifiedLength > 0; - while (change.originalStart + change.originalLength < originalStop && - change.modifiedStart + change.modifiedLength < modifiedStop && - (!checkOriginal || this.OriginalElementsAreEqual(change.originalStart, change.originalStart + change.originalLength)) && - (!checkModified || this.ModifiedElementsAreEqual(change.modifiedStart, change.modifiedStart + change.modifiedLength))) { + while ( + change.originalStart + change.originalLength < originalStop + && change.modifiedStart + change.modifiedLength < modifiedStop + && (!checkOriginal || this.OriginalElementsAreEqual(change.originalStart, change.originalStart + change.originalLength)) + && (!checkModified || this.ModifiedElementsAreEqual(change.modifiedStart, change.modifiedStart + change.modifiedLength)) + ) { + const startStrictEqual = this.ElementsAreStrictEqual(change.originalStart, change.modifiedStart); + const endStrictEqual = this.ElementsAreStrictEqual(change.originalStart + change.originalLength, change.modifiedStart + change.modifiedLength); + if (endStrictEqual && !startStrictEqual) { + // moving the change down would create an equal change, but the elements are not strict equal + break; + } change.originalStart++; change.modifiedStart++; } diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index adda7b1c2c..c88e86f697 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -68,6 +68,7 @@ export namespace Event { * Given an event and a `filter` function, returns another event which emits those * elements for which the `filter` function returns `true`. */ + export function filter(event: Event, filter: (e: T | U) => e is T): Event; export function filter(event: Event, filter: (e: T) => boolean): Event; export function filter(event: Event, filter: (e: T | R) => e is R): Event; export function filter(event: Event, filter: (e: T) => boolean): Event { @@ -188,18 +189,29 @@ export namespace Event { * Given an event, it returns another event which fires only when the event * element changes. */ - export function latch(event: Event): Event { + export function latch(event: Event, equals: (a: T, b: T) => boolean = (a, b) => a === b): Event { let firstCall = true; let cache: T; return filter(event, value => { - const shouldEmit = firstCall || value !== cache; + const shouldEmit = firstCall || !equals(value, cache); firstCall = false; cache = value; return shouldEmit; }); } + /** + * Given an event, it returns another event which fires only when the event + * element changes. + */ + export function split(event: Event, isT: (e: T | U) => e is T): [Event, Event] { + return [ + Event.filter(event, isT), + Event.filter(event, e => !isT(e)) as Event, + ]; + } + /** * Buffers the provided event until a first listener comes * along, at which point fire all the events at once and @@ -633,10 +645,13 @@ export class Emitter { } dispose() { - this._listeners?.clear(); - this._deliveryQueue?.clear(); - this._leakageMon?.dispose(); - this._disposed = true; + if (!this._disposed) { + this._disposed = true; + this._listeners?.clear(); + this._deliveryQueue?.clear(); + this._options?.onLastListenerRemove?.(); + this._leakageMon?.dispose(); + } } } diff --git a/src/vs/base/common/extpath.ts b/src/vs/base/common/extpath.ts index 3f47f06f74..aa30657da9 100644 --- a/src/vs/base/common/extpath.ts +++ b/src/vs/base/common/extpath.ts @@ -22,6 +22,23 @@ export function toSlashes(osPath: string) { return osPath.replace(/[\\/]/g, posix.sep); } +/** + * Takes a Windows OS path (using backward or forward slashes) and turns it into a posix path: + * - turns backward slashes into forward slashes + * - makes it absolute if it starts with a drive letter + * This should only be done for OS paths from Windows (or user provided paths potentially from Windows). + * Using it on a Linux or MaxOS path might change it. + */ +export function toPosixPath(osPath: string) { + if (osPath.indexOf('/') === -1) { + osPath = toSlashes(osPath); + } + if (/^[a-zA-Z]:(\/|$)/.test(osPath)) { // starts with a drive letter + osPath = '/' + osPath; + } + return osPath; +} + /** * Computes the _root_ this path, like `getRoot('c:\files') === c:\`, * `getRoot('files:///files/path') === files:///`, diff --git a/src/vs/base/common/filters.ts b/src/vs/base/common/filters.ts index b04f4d845a..53209e23e9 100644 --- a/src/vs/base/common/filters.ts +++ b/src/vs/base/common/filters.ts @@ -526,7 +526,7 @@ const enum Arrow { Diag = 1, Left = 2, LeftLeft = 3 } * 3. `` * 4. `` etc */ -export type FuzzyScore = [score: number, wordStart: number, ...matches: number[]];// [number, number, number]; +export type FuzzyScore = [score: number, wordStart: number, ...matches: number[]]; export namespace FuzzyScore { /** diff --git a/src/vs/base/common/htmlContent.ts b/src/vs/base/common/htmlContent.ts index fe384023e1..d85d87a5c4 100644 --- a/src/vs/base/common/htmlContent.ts +++ b/src/vs/base/common/htmlContent.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { equals } from 'vs/base/common/arrays'; import { UriComponents } from 'vs/base/common/uri'; import { escapeIcons } from 'vs/base/common/iconLabels'; import { illegalArgument } from 'vs/base/common/errors'; @@ -90,21 +89,7 @@ export function isMarkdownString(thing: any): thing is IMarkdownString { return false; } -export function markedStringsEquals(a: IMarkdownString | IMarkdownString[], b: IMarkdownString | IMarkdownString[]): boolean { - if (!a && !b) { - return true; - } else if (!a || !b) { - return false; - } else if (Array.isArray(a) && Array.isArray(b)) { - return equals(a, b, markdownStringEqual); - } else if (isMarkdownString(a) && isMarkdownString(b)) { - return markdownStringEqual(a, b); - } else { - return false; - } -} - -function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean { +export function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean { if (a === b) { return true; } else if (!a || !b) { diff --git a/src/vs/base/common/idGenerator.ts b/src/vs/base/common/idGenerator.ts index de66b6da4b..1ff53213a2 100644 --- a/src/vs/base/common/idGenerator.ts +++ b/src/vs/base/common/idGenerator.ts @@ -18,4 +18,4 @@ export class IdGenerator { } } -export const defaultGenerator = new IdGenerator('id#'); \ No newline at end of file +export const defaultGenerator = new IdGenerator('id#'); diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index 83b529768d..99f9a6c65d 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -30,7 +30,7 @@ export namespace Iterable { return iterable[Symbol.iterator]().next().value; } - export function some(iterable: Iterable, predicate: (t: T) => boolean): boolean { + export function some(iterable: Iterable, predicate: (t: T) => unknown): boolean { for (const element of iterable) { if (predicate(element)) { return true; @@ -61,9 +61,10 @@ export namespace Iterable { } } - export function* map(iterable: Iterable, fn: (t: T) => R): Iterable { + export function* map(iterable: Iterable, fn: (t: T, index: number) => R): Iterable { + let index = 0; for (const element of iterable) { - yield fn(element); + yield fn(element, index++); } } diff --git a/src/vs/base/common/jsonErrorMessages.ts b/src/vs/base/common/jsonErrorMessages.ts index 1b3086f740..e43d2c8753 100644 --- a/src/vs/base/common/jsonErrorMessages.ts +++ b/src/vs/base/common/jsonErrorMessages.ts @@ -23,4 +23,4 @@ export function getParseErrorMessage(errorCode: ParseErrorCode): string { default: return ''; } -} \ No newline at end of file +} diff --git a/src/vs/base/common/keybindingParser.ts b/src/vs/base/common/keybindingParser.ts index 8dec67a921..3ae274d35d 100644 --- a/src/vs/base/common/keybindingParser.ts +++ b/src/vs/base/common/keybindingParser.ts @@ -121,4 +121,4 @@ export class KeybindingParser { } return parts; } -} \ No newline at end of file +} diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index bb57ebc584..8edf88fc79 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -283,6 +283,31 @@ export abstract class ReferenceCollection { protected abstract destroyReferencedObject(key: string, object: T): void; } +/** + * Unwraps a reference collection of promised values. Makes sure + * references are disposed whenever promises get rejected. + */ +export class AsyncReferenceCollection { + + constructor(private referenceCollection: ReferenceCollection>) { } + + async acquire(key: string, ...args: any[]): Promise> { + const ref = this.referenceCollection.acquire(key, ...args); + + try { + const object = await ref.object; + + return { + object, + dispose: () => ref.dispose() + }; + } catch (error) { + ref.dispose(); + throw error; + } + } +} + export class ImmortalReference implements IReference { constructor(public object: T) { } dispose(): void { /* noop */ } diff --git a/src/vs/base/common/linkedList.ts b/src/vs/base/common/linkedList.ts index 2d4f27fc7b..c39a3c5ef9 100644 --- a/src/vs/base/common/linkedList.ts +++ b/src/vs/base/common/linkedList.ts @@ -33,6 +33,14 @@ export class LinkedList { } clear(): void { + let node = this._first; + while (node !== Node.Undefined) { + const next = node.next; + node.prev = Node.Undefined; + node.next = Node.Undefined; + node = next; + } + this._first = Node.Undefined; this._last = Node.Undefined; this._size = 0; diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index 822d8e775d..56eea38655 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -498,20 +498,27 @@ export class TernarySearchTree { } private *_entries(node: TernarySearchTreeNode | undefined): IterableIterator<[K, V]> { - if (node) { - // left - yield* this._entries(node.left); - - // node - if (node.value) { - // callback(node.value, this._iter.join(parts)); - yield [node.key, node.value]; + // DFS + if (!node) { + return; + } + const stack = [node]; + while (stack.length > 0) { + const node = stack.pop(); + if (node) { + if (node.value) { + yield [node.key, node.value]; + } + if (node.left) { + stack.push(node.left); + } + if (node.mid) { + stack.push(node.mid); + } + if (node.right) { + stack.push(node.right); + } } - // mid - yield* this._entries(node.mid); - - // right - yield* this._entries(node.right); } } } diff --git a/src/vs/base/common/marked/marked.js b/src/vs/base/common/marked/marked.js index 678a30380a..163c49df39 100644 --- a/src/vs/base/common/marked/marked.js +++ b/src/vs/base/common/marked/marked.js @@ -1,6 +1,6 @@ /** * marked - a markdown parser - * Copyright (c) 2011-2020, Christopher Jeffrey. (Source EULAd) + * Copyright (c) 2011-2021, Christopher Jeffrey. (Source EULAd) * https://github.com/markedjs/marked */ diff --git a/src/vs/base/common/mime.ts b/src/vs/base/common/mime.ts index 51c1d96df7..98c5c240cb 100644 --- a/src/vs/base/common/mime.ts +++ b/src/vs/base/common/mime.ts @@ -316,3 +316,20 @@ export function getExtensionForMimeType(mimeType: string): string | undefined { return undefined; } + +const _simplePattern = /^(.+)\/(.+?)(;.+)?$/; + +export function normalizeMimeType(mimeType: string): string; +export function normalizeMimeType(mimeType: string, strict: true): string | undefined; +export function normalizeMimeType(mimeType: string, strict?: true): string | undefined { + + const match = _simplePattern.exec(mimeType); + if (!match) { + return strict + ? undefined + : mimeType; + } + // https://datatracker.ietf.org/doc/html/rfc2045#section-5.1 + // media and subtype must ALWAYS be lowercase, parameter not + return `${match[1].toLowerCase()}/${match[2].toLowerCase()}${match[3] ?? ''}`; +} diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index 23a88b7c38..0f059da2be 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -61,6 +61,7 @@ export namespace Schemas { export const vscodeNotebookCell = 'vscode-notebook-cell'; export const vscodeNotebookCellMetadata = 'vscode-notebook-cell-metadata'; + export const vscodeNotebookCellOutput = 'vscode-notebook-cell-output'; export const vscodeSettings = 'vscode-settings'; diff --git a/src/vs/base/common/objects.ts b/src/vs/base/common/objects.ts index 3bf23d4904..ebc76ffa44 100644 --- a/src/vs/base/common/objects.ts +++ b/src/vs/base/common/objects.ts @@ -113,18 +113,6 @@ export function mixin(destination: any, source: any, overwrite: boolean = true): return destination; } -/** - * @deprecated ES6 - */ -export function assign(destination: T): T; -export function assign(destination: T, u: U): T & U; -export function assign(destination: T, u: U, v: V): T & U & V; -export function assign(destination: T, u: U, v: V, w: W): T & U & V & W; -export function assign(destination: any, ...sources: any[]): any { - sources.forEach(source => Object.keys(source).forEach(key => destination[key] = source[key])); - return destination; -} - export function equals(one: any, other: any): boolean { if (one === other) { return true; @@ -238,3 +226,13 @@ export function getCaseInsensitive(target: obj, key: string): any { const equivalentKey = Object.keys(target).find(k => k.toLowerCase() === lowercaseKey); return equivalentKey ? target[equivalentKey] : target[key]; } + +export function filter(obj: obj, predicate: (key: string, value: any) => boolean): obj { + const result = Object.create(null); + for (const key of Object.keys(obj)) { + if (predicate(key, obj[key])) { + result[key] = obj[key]; + } + } + return result; +} diff --git a/src/vs/base/common/parsers.ts b/src/vs/base/common/parsers.ts index 8505be9cfd..950c062547 100644 --- a/src/vs/base/common/parsers.ts +++ b/src/vs/base/common/parsers.ts @@ -76,4 +76,4 @@ export abstract class Parser { public fatal(message: string): void { this._problemReporter.fatal(message); } -} \ No newline at end of file +} diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts index cef570a0de..7c0297e415 100644 --- a/src/vs/base/common/platform.ts +++ b/src/vs/base/common/platform.ts @@ -53,7 +53,7 @@ declare const self: unknown; export const globals: any = (typeof self === 'object' ? self : typeof global === 'object' ? global : {}); let nodeProcess: INodeProcess | undefined = undefined; -if (typeof globals.vscode !== 'undefined') { +if (typeof globals.vscode !== 'undefined' && typeof globals.vscode.process !== 'undefined') { // Native environment (sandboxed) nodeProcess = globals.vscode.process; } else if (typeof process !== 'undefined') { diff --git a/src/vs/base/common/ports.ts b/src/vs/base/common/ports.ts new file mode 100644 index 0000000000..5cca1c8d48 --- /dev/null +++ b/src/vs/base/common/ports.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * @returns Returns a random port between 1025 and 65535. + */ +export function randomPort(): number { + const min = 1025; + const max = 65535; + return min + Math.floor((max - min) * Math.random()); +} diff --git a/src/vs/base/common/process.ts b/src/vs/base/common/process.ts index e6e453ce72..98a83a750b 100644 --- a/src/vs/base/common/process.ts +++ b/src/vs/base/common/process.ts @@ -9,7 +9,7 @@ let safeProcess: INodeProcess & { nextTick: (callback: (...args: any[]) => void) declare const process: INodeProcess; // Native sandbox environment -if (typeof globals.vscode !== 'undefined') { +if (typeof globals.vscode !== 'undefined' && typeof globals.vscode.process !== 'undefined') { const sandboxProcess: INodeProcess = globals.vscode.process; safeProcess = { get platform() { return sandboxProcess.platform; }, @@ -38,7 +38,7 @@ else { nextTick(callback: (...args: any[]) => void): void { return setImmediate(callback); }, // Unsupported - get env() { return Object.create(null); }, + get env() { return {}; }, cwd() { return '/'; } }; } diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 1e38ac9062..48f83c25d8 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IStringDictionary } from 'vs/base/common/collections'; @@ -25,6 +25,11 @@ export type ExtensionUntrustedWorkspaceSupport = { readonly override?: boolean | 'limited' }; +export type ExtensionVirtualWorkspaceSupport = { + readonly default?: boolean, + readonly override?: boolean +}; + export interface IProductConfiguration { readonly version: string; readonly date?: string; @@ -74,6 +79,7 @@ export interface IProductConfiguration { readonly remoteExtensionTips?: { [remoteName: string]: IRemoteExtensionTip; }; readonly extensionKeywords?: { [extension: string]: readonly string[]; }; readonly keymapExtensionTips?: readonly string[]; + readonly languageExtensionTips?: readonly string[]; readonly trustedExtensionUrlPublicKeys?: { [id: string]: string[]; }; readonly recommendedExtensions: string[]; // {{SQL CARBON EDIT}} @@ -128,7 +134,7 @@ export interface IProductConfiguration { readonly extensionSyncedKeys?: { readonly [extensionId: string]: string[]; }; readonly extensionAllowedProposedApi?: readonly string[]; readonly extensionUntrustedWorkspaceSupport?: { readonly [extensionId: string]: ExtensionUntrustedWorkspaceSupport }; - readonly extensionVirtualWorkspacesSupport?: { readonly [extensionId: string]: { default?: boolean, override?: boolean } }; + readonly extensionVirtualWorkspacesSupport?: { readonly [extensionId: string]: ExtensionVirtualWorkspaceSupport }; readonly msftInternalDomains?: string[]; readonly linkProtectionTrustedDomains?: readonly string[]; @@ -136,6 +142,8 @@ export interface IProductConfiguration { readonly 'configurationSync.store'?: ConfigurationSyncStore; readonly darwinUniversalAssetId?: string; + + readonly webviewContentExternalBaseUrlTemplate?: string; } export type ImportantExtensionTip = { name: string; languages?: string[]; pattern?: string; isExtensionPack?: boolean }; diff --git a/src/vs/base/common/resources.ts b/src/vs/base/common/resources.ts index 8cd1cb21ab..b21113df38 100644 --- a/src/vs/base/common/resources.ts +++ b/src/vs/base/common/resources.ts @@ -50,7 +50,7 @@ export interface IExtUri { /** * Creates a key from a resource URI to be used to resource comparison and for resource maps. - * @see ResourceMap + * @see {@link ResourceMap} * @param uri Uri * @param ignoreFragment Ignore the fragment (defaults to `false`) */ @@ -264,12 +264,7 @@ export class ExtUri implements IExtUri { path: newURI.path }); } - if (path.indexOf('/') === -1) { // no slashes? it's likely a Windows path - path = extpath.toSlashes(path); - if (/^[a-zA-Z]:(\/|$)/.test(path)) { // starts with a drive letter - path = '/' + path; - } - } + path = extpath.toPosixPath(path); // we allow path to be a windows path return base.with({ path: paths.posix.resolve(base.path, path) }); diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index 32cdc31789..86fb87a70f 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -152,41 +152,6 @@ export function stripWildcards(pattern: string): string { return pattern.replace(/\*/g, ''); } -/** - * @deprecated ES6: use `String.startsWith` - */ -export function startsWith(haystack: string, needle: string): boolean { - if (haystack.length < needle.length) { - return false; - } - - if (haystack === needle) { - return true; - } - - for (let i = 0; i < needle.length; i++) { - if (haystack[i] !== needle[i]) { - return false; - } - } - - return true; -} - -/** - * @deprecated ES6: use `String.endsWith` - */ -export function endsWith(haystack: string, needle: string): boolean { - const diff = haystack.length - needle.length; - if (diff > 0) { - return haystack.indexOf(needle, diff) === diff; - } else if (diff === 0) { - return haystack === needle; - } else { - return false; - } -} - export interface RegExpOptions { matchCase?: boolean; wholeWord?: boolean; @@ -1129,3 +1094,81 @@ function getGraphemeBreakRawData(): number[] { } //#endregion + +/** + * Computes the offset after performing a left delete on the given string, + * while considering unicode grapheme/emoji rules. +*/ +export function getLeftDeleteOffset(offset: number, str: string): number { + if (offset === 0) { + return 0; + } + + // Try to delete emoji part. + const emojiOffset = getOffsetBeforeLastEmojiComponent(offset, str); + if (emojiOffset !== undefined) { + return emojiOffset; + } + + // Otherwise, just skip a single code point. + const codePoint = getPrevCodePoint(str, offset); + offset -= getUTF16Length(codePoint); + return offset; +} + +function getOffsetBeforeLastEmojiComponent(offset: number, str: string): number | undefined { + // See https://www.unicode.org/reports/tr51/tr51-14.html#EBNF_and_Regex for the + // structure of emojis. + let codePoint = getPrevCodePoint(str, offset); + offset -= getUTF16Length(codePoint); + + // Skip modifiers + while ((isEmojiModifier(codePoint) || codePoint === CodePoint.emojiVariantSelector || codePoint === CodePoint.enclosingKeyCap)) { + if (offset === 0) { + // Cannot skip modifier, no preceding emoji base. + return undefined; + } + codePoint = getPrevCodePoint(str, offset); + offset -= getUTF16Length(codePoint); + } + + // Expect base emoji + if (!isEmojiImprecise(codePoint)) { + // Unexpected code point, not a valid emoji. + return undefined; + } + + if (offset >= 0) { + // Skip optional ZWJ code points that combine multiple emojis. + // In theory, we should check if that ZWJ actually combines multiple emojis + // to prevent deleting ZWJs in situations we didn't account for. + const optionalZwjCodePoint = getPrevCodePoint(str, offset); + if (optionalZwjCodePoint === CodePoint.zwj) { + offset -= getUTF16Length(optionalZwjCodePoint); + } + } + + return offset; +} + +function getUTF16Length(codePoint: number) { + return codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1; +} + +function isEmojiModifier(codePoint: number): boolean { + return 0x1F3FB <= codePoint && codePoint <= 0x1F3FF; +} + +const enum CodePoint { + zwj = 0x200D, + + /** + * Variation Selector-16 (VS16) + */ + emojiVariantSelector = 0xFE0F, + + /** + * Combining Enclosing Keycap + */ + enclosingKeyCap = 0x20E3, +} diff --git a/src/vs/base/common/uri.ts b/src/vs/base/common/uri.ts index 40a0d29936..8728c127dc 100644 --- a/src/vs/base/common/uri.ts +++ b/src/vs/base/common/uri.ts @@ -327,13 +327,15 @@ export class URI implements UriComponents { } static from(components: { scheme: string; authority?: string; path?: string; query?: string; fragment?: string }): URI { - return new Uri( + const result = new Uri( components.scheme, components.authority, components.path, components.query, components.fragment, ); + _validateUri(result, true); + return result; } /** diff --git a/src/vs/base/common/uriIpc.ts b/src/vs/base/common/uriIpc.ts index 7cfc81703b..edab39757d 100644 --- a/src/vs/base/common/uriIpc.ts +++ b/src/vs/base/common/uriIpc.ts @@ -152,4 +152,4 @@ export function transformAndReviveIncomingURIs(obj: T, transformer: IURITrans return obj; } return result; -} \ No newline at end of file +} diff --git a/src/vs/base/node/extpath.ts b/src/vs/base/node/extpath.ts index 72a2cca462..8c0c39cfe6 100644 --- a/src/vs/base/node/extpath.ts +++ b/src/vs/base/node/extpath.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; -import { promisify } from 'util'; 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 { Promises, readdirSync } from 'vs/base/node/pfs'; /** * Copied from: https://github.com/microsoft/vscode-node-debug/blob/master/src/node/pathUtilities.ts#L83 @@ -57,7 +56,7 @@ export async function realpath(path: string): Promise { // calls `fs.native.realpath` which will result in subst // drives to be resolved to their target on Windows // https://github.com/microsoft/vscode/issues/118562 - return await promisify(fs.realpath)(path); + return await Promises.realpath(path); } catch (error) { // We hit an error calling fs.realpath(). Since fs.realpath() is doing some path normalization @@ -67,7 +66,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 fs.promises.access(normalizedPath, fs.constants.R_OK); + await Promises.access(normalizedPath, fs.constants.R_OK); return normalizedPath; } diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index d49d0e65d1..93ce68ee19 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -5,6 +5,7 @@ import * as fs from 'fs'; import { tmpdir } from 'os'; +import { promisify } from 'util'; import { join } from 'vs/base/common/path'; import { ResourceQueue } from 'vs/base/common/async'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; @@ -38,7 +39,7 @@ export enum RimRafMode { * - `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 { +async function rimraf(path: string, mode = RimRafMode.UNLINK): Promise { if (isRootOrDriveLetter(path)) { throw new Error('rimraf - will refuse to recursively delete root'); } @@ -56,7 +57,7 @@ async function rimrafMove(path: string): Promise { try { const pathInTemp = join(tmpdir(), generateUuid()); try { - await fs.promises.rename(path, pathInTemp); + await Promises.rename(path, pathInTemp); } catch (error) { return rimrafUnlink(path); // if rename fails, delete without tmp dir } @@ -71,7 +72,7 @@ async function rimrafMove(path: string): Promise { } async function rimrafUnlink(path: string): Promise { - return fs.promises.rmdir(path, { recursive: true, maxRetries: 3 }); + return Promises.rmdir(path, { recursive: true, maxRetries: 3 }); } export function rimrafSync(path: string): void { @@ -99,15 +100,15 @@ export interface IDirent { * 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 readdir(path: string): Promise; +async function readdir(path: string, options: { withFileTypes: true }): Promise; +async function readdir(path: string, options?: { withFileTypes: true }): Promise<(string | IDirent)[]> { + return handleDirectoryChildren(await (options ? safeReaddirWithFileTypes(path) : promisify(fs.readdir)(path))); } async function safeReaddirWithFileTypes(path: string): Promise { try { - return await fs.promises.readdir(path, { withFileTypes: true }); + return await promisify(fs.readdir)(path, { withFileTypes: true }); } catch (error) { console.warn('[node.js fs] readdir with filetypes failed with error: ', error); } @@ -126,7 +127,7 @@ async function safeReaddirWithFileTypes(path: string): Promise { let isSymbolicLink = false; try { - const lstat = await fs.promises.lstat(join(path, child)); + const lstat = await Promises.lstat(join(path, child)); isFile = lstat.isFile(); isDirectory = lstat.isDirectory(); @@ -178,7 +179,7 @@ function handleDirectoryChildren(children: (string | IDirent)[]): (string | IDir * A convinience method to read all children of a path that * are directories. */ -export async function readDirsInDir(dirPath: string): Promise { +async function readDirsInDir(dirPath: string): Promise { const children = await readdir(dirPath); const directories: string[] = []; @@ -251,7 +252,7 @@ export namespace SymlinkSupport { // First stat the link let lstats: fs.Stats | undefined; try { - lstats = await fs.promises.lstat(path); + lstats = await Promises.lstat(path); // Return early if the stat is not a symbolic link at all if (!lstats.isSymbolicLink()) { @@ -264,7 +265,7 @@ export namespace SymlinkSupport { // 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); + const stats = await Promises.stat(path); return { stat: stats, symbolicLink: lstats?.isSymbolicLink() ? { dangling: false } : undefined }; } catch (error) { @@ -279,7 +280,7 @@ export namespace SymlinkSupport { // 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)); + const stats = await Promises.stat(await Promises.readlink(path)); return { stat: stats, symbolicLink: { dangling: false } }; } catch (error) { @@ -359,11 +360,11 @@ const writeQueues = new ResourceQueue(); * * 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; -export function writeFile(path: string, data: string | Buffer | Uint8Array, options?: IWriteFileOptions): Promise; -export function writeFile(path: string, data: string | Buffer | Uint8Array, options?: IWriteFileOptions): Promise { +function writeFile(path: string, data: string, options?: IWriteFileOptions): Promise; +function writeFile(path: string, data: Buffer, options?: IWriteFileOptions): Promise; +function writeFile(path: string, data: Uint8Array, options?: IWriteFileOptions): Promise; +function writeFile(path: string, data: string | Buffer | Uint8Array, options?: IWriteFileOptions): Promise; +function writeFile(path: string, data: string | Buffer | Uint8Array, options?: IWriteFileOptions): Promise { return writeQueues.queueFor(URI.file(path), extUriBiasedIgnorePathCase).queue(() => { const ensuredOptions = ensureWriteOptions(options); @@ -371,7 +372,7 @@ export function writeFile(path: string, data: string | Buffer | Uint8Array, opti }); } -export interface IWriteFileOptions { +interface IWriteFileOptions { mode?: number; flag?: string; } @@ -473,7 +474,7 @@ function ensureWriteOptions(options?: IWriteFileOptions): IEnsuredWriteFileOptio * - updates the `mtime` of the `source` after the operation * - allows to move across multiple disks */ -export async function move(source: string, target: string): Promise { +async function move(source: string, target: string): Promise { if (source === target) { return; // simulate node.js behaviour here and do a no-op if paths match } @@ -488,24 +489,19 @@ export async function move(source: string, target: string): Promise { // as well because conceptually it is a change of a similar category. async function updateMtime(path: string): Promise { try { - const stat = await fs.promises.lstat(path); + const stat = await Promises.lstat(path); if (stat.isDirectory() || stat.isSymbolicLink()) { return; // only for files } - const fh = await fs.promises.open(path, 'a'); - try { - await fh.utimes(stat.atime, new Date()); - } finally { - await fh.close(); - } + await Promises.utimes(path, stat.atime, new Date()); } catch (error) { // Ignore any error } } try { - await fs.promises.rename(source, target); + await Promises.rename(source, target); await updateMtime(target); } catch (error) { @@ -540,7 +536,7 @@ interface ICopyPayload { * 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 { +async function copy(source: string, target: string, options: { preserveSymlinks: boolean }): Promise { return doCopy(source, target, { root: { source, target }, options, handledSourcePaths: new Set() }); } @@ -594,7 +590,7 @@ async function doCopy(source: string, target: string, payload: ICopyPayload): Pr async function doCopyDirectory(source: string, target: string, mode: number, payload: ICopyPayload): Promise { // Create folder - await fs.promises.mkdir(target, { recursive: true, mode }); + await Promises.mkdir(target, { recursive: true, mode }); // Copy each file recursively const files = await readdir(source); @@ -606,16 +602,16 @@ async function doCopyDirectory(source: string, target: string, mode: number, pay async function doCopyFile(source: string, target: string, mode: number): Promise { // Copy file - await fs.promises.copyFile(source, target); + await Promises.copyFile(source, target); // restore mode (https://github.com/nodejs/node/issues/1104) - await fs.promises.chmod(target, mode); + await 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); + let linkTarget = await Promises.readlink(source); // Special case: the symlink points to a target that is // actually within the path that is being copied. In that @@ -626,21 +622,92 @@ async function doCopySymlink(source: string, target: string, payload: ICopyPaylo } // Create symlink - await fs.promises.symlink(linkTarget, target); + await Promises.symlink(linkTarget, target); } //#endregion -//#region Async FS Methods +//#region Promise based fs methods -export async function exists(path: string): Promise { - try { - await fs.promises.access(path); +/** + * Prefer this helper class over the `fs.promises` API to + * enable `graceful-fs` to function properly. Given issue + * https://github.com/isaacs/node-graceful-fs/issues/160 it + * is evident that the module only takes care of the non-promise + * based fs methods. + * + * Another reason is `realpath` being entirely different in + * the promise based implementation compared to the other + * one (https://github.com/microsoft/vscode/issues/118562) + * + * Note: using getters for a reason, since `graceful-fs` + * patching might kick in later after modules have been + * loaded we need to defer access to fs methods. + * (https://github.com/microsoft/vscode/issues/124176) + */ +export const Promises = new class { - return true; - } catch { - return false; + //#region Implemented by node.js + + get access() { return promisify(fs.access); } + + get stat() { return promisify(fs.stat); } + get lstat() { return promisify(fs.lstat); } + get utimes() { return promisify(fs.utimes); } + + get read() { return promisify(fs.read); } + get readFile() { return promisify(fs.readFile); } + + get write() { return promisify(fs.write); } + + get appendFile() { return promisify(fs.appendFile); } + + get fdatasync() { return promisify(fs.fdatasync); } + get truncate() { return promisify(fs.truncate); } + + get rename() { return promisify(fs.rename); } + get copyFile() { return promisify(fs.copyFile); } + + get open() { return promisify(fs.open); } + get close() { return promisify(fs.close); } + + get symlink() { return promisify(fs.symlink); } + get readlink() { return promisify(fs.readlink); } + + get chmod() { return promisify(fs.chmod); } + + get mkdir() { return promisify(fs.mkdir); } + + get unlink() { return promisify(fs.unlink); } + get rmdir() { return promisify(fs.rmdir); } + + get realpath() { return promisify(fs.realpath); } + + //#endregion + + //#region Implemented by us + + async exists(path: string): Promise { + try { + await Promises.access(path); + + return true; + } catch { + return false; + } } -} + + get readdir() { return readdir; } + get readDirsInDir() { return readDirsInDir; } + + get writeFile() { return writeFile; } + + get rm() { return rimraf; } + + get move() { return move; } + get copy() { return copy; } + + //#endregion +}; //#endregion diff --git a/src/vs/base/node/ports.ts b/src/vs/base/node/ports.ts index 6a87c0bc5d..b5645be431 100644 --- a/src/vs/base/node/ports.ts +++ b/src/vs/base/node/ports.ts @@ -5,15 +5,6 @@ import * as net from 'net'; -/** - * @returns Returns a random port between 1025 and 65535. - */ -export function randomPort(): number { - const min = 1025; - const max = 65535; - return min + Math.floor((max - min) * Math.random()); -} - /** * Given a start point and a max number of retries, will find a port that * is openable. Will return 0 in case no free port can be found. diff --git a/src/vs/base/node/powershell.ts b/src/vs/base/node/powershell.ts index 17d0e98705..abf797d192 100644 --- a/src/vs/base/node/powershell.ts +++ b/src/vs/base/node/powershell.ts @@ -137,7 +137,7 @@ async function findPSCoreWindowsInstallation( let highestSeenVersion: number = -1; let pwshExePath: string | null = null; - for (const item of await pfs.readdir(powerShellInstallBaseDir)) { + for (const item of await pfs.Promises.readdir(powerShellInstallBaseDir)) { let currentVersion: number = -1; if (findPreview) { @@ -210,7 +210,7 @@ async function findPSCoreMsix({ findPreview }: { findPreview?: boolean } = {}): : { 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)) { + for (const subdir of await pfs.Promises.readdir(msixAppDir)) { if (pwshMsixDirRegex.test(subdir)) { const pwshMsixPath = path.join(msixAppDir, subdir, 'pwsh.exe'); return new PossiblePowerShellExe(pwshMsixPath, pwshMsixName); diff --git a/src/vs/base/node/processes.ts b/src/vs/base/node/processes.ts index c90536da30..7874612e80 100644 --- a/src/vs/base/node/processes.ts +++ b/src/vs/base/node/processes.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as path from 'vs/base/common/path'; -import * as fs from 'fs'; import * as pfs from 'vs/base/node/pfs'; import * as cp from 'child_process'; import * as nls from 'vs/nls'; @@ -457,8 +456,8 @@ export namespace win32 { } async function fileExists(path: string): Promise { - if (await pfs.exists(path)) { - return !((await fs.promises.stat(path)).isDirectory()); + if (await pfs.Promises.exists(path)) { + return !((await pfs.Promises.stat(path)).isDirectory()); } return false; } diff --git a/src/vs/base/node/watcher.ts b/src/vs/base/node/watcher.ts index be60644e51..9d1008bf04 100644 --- a/src/vs/base/node/watcher.ts +++ b/src/vs/base/node/watcher.ts @@ -8,7 +8,7 @@ import { watch } from 'fs'; import { isMacintosh } from 'vs/base/common/platform'; import { normalizeNFC } from 'vs/base/common/normalization'; import { toDisposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { exists, readdir } from 'vs/base/node/pfs'; +import { Promises } from 'vs/base/node/pfs'; export function watchFile(path: string, onChange: (type: 'added' | 'changed' | 'deleted', path: string) => void, onError: (error: string) => void): IDisposable { return doWatchNonRecursive({ path, isDirectory: false }, onChange, onError); @@ -42,7 +42,7 @@ function doWatchNonRecursive(file: { path: string, isDirectory: boolean }, onCha // Folder: resolve children to emit proper events const folderChildren: Set = new Set(); if (file.isDirectory) { - readdir(file.path).then(children => children.forEach(child => folderChildren.add(child))); + Promises.readdir(file.path).then(children => children.forEach(child => folderChildren.add(child))); } watcher.on('error', (code: number, signal: string) => { @@ -87,7 +87,7 @@ function doWatchNonRecursive(file: { path: string, isDirectory: boolean }, onCha // does indeed not exist anymore. const timeoutHandle = setTimeout(async () => { - const fileExists = await exists(changedFilePath); + const fileExists = await Promises.exists(changedFilePath); if (disposed) { return; // ignore if disposed by now @@ -131,7 +131,7 @@ function doWatchNonRecursive(file: { path: string, isDirectory: boolean }, onCha const timeoutHandle = setTimeout(async () => { mapPathToStatDisposable.delete(changedFilePath); - const fileExists = await exists(changedFilePath); + const fileExists = await Promises.exists(changedFilePath); if (disposed) { return; // ignore if disposed by now @@ -177,7 +177,7 @@ function doWatchNonRecursive(file: { path: string, isDirectory: boolean }, onCha } }); } catch (error) { - exists(file.path).then(exists => { + Promises.exists(file.path).then(exists => { if (exists && !disposed) { onError(`Failed to watch ${file.path} for changes using fs.watch() (${error.toString()})`); } diff --git a/src/vs/base/node/zip.ts b/src/vs/base/node/zip.ts index aa2f49bcd3..4f778d191a 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 { promises, createWriteStream, WriteStream } from 'fs'; +import { createWriteStream, WriteStream } from 'fs'; import { Readable } from 'stream'; import { Sequencer, createCancelablePromise } from 'vs/base/common/async'; -import { rimraf } from 'vs/base/node/pfs'; +import { Promises } 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(promises.mkdir(targetDirName, { recursive: true })).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 => promises.mkdir(targetFileName, { recursive: true }).then(() => readNextEntry(token)).then(undefined, e)); + last = createCancelablePromise(token => Promises.mkdir(targetFileName, { recursive: true }).then(() => readNextEntry(token)).then(undefined, e)); return; } @@ -218,7 +218,7 @@ export function extract(zipPath: string, targetPath: string, options: IExtractOp let promise = openZip(zipPath, true); if (options.overwrite) { - promise = promise.then(zipfile => rimraf(targetPath).then(() => zipfile)); + promise = promise.then(zipfile => Promises.rm(targetPath).then(() => zipfile)); } return promise.then(zipfile => extractZip(zipfile, targetPath, { sourcePathRegex }, token)); diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index 97e5f74246..894d1bdaae 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -13,6 +13,7 @@ import { getRandomElement } from 'vs/base/common/arrays'; import { isFunction, isUndefinedOrNull } from 'vs/base/common/types'; import { revive } from 'vs/base/common/marshalling'; import * as strings from 'vs/base/common/strings'; +import { memoize } from 'vs/base/common/decorators'; /** * An `IChannel` is an abstraction over a collection of commands. @@ -723,11 +724,16 @@ export class ChannelClient implements IChannelClient, IDisposable { } } + @memoize + get onDidInitializePromise(): Promise { + return Event.toPromise(this.onDidInitialize); + } + private whenInitialized(): Promise { if (this.state === State.Idle) { return Promise.resolve(); } else { - return Event.toPromise(this.onDidInitialize); + return this.onDidInitializePromise; } } diff --git a/src/vs/base/parts/ipc/test/node/testApp.ts b/src/vs/base/parts/ipc/test/node/testApp.ts index 7a95f1995f..d242b88426 100644 --- a/src/vs/base/parts/ipc/test/node/testApp.ts +++ b/src/vs/base/parts/ipc/test/node/testApp.ts @@ -8,4 +8,4 @@ import { TestChannel, TestService } from './testService'; const server = new Server('test'); const service = new TestService(); -server.registerChannel('test', new TestChannel(service)); \ No newline at end of file +server.registerChannel('test', new TestChannel(service)); diff --git a/src/vs/base/parts/quickinput/browser/media/quickInput.css b/src/vs/base/parts/quickinput/browser/media/quickInput.css index 3d5bce9a14..10b62e4e0e 100644 --- a/src/vs/base/parts/quickinput/browser/media/quickInput.css +++ b/src/vs/base/parts/quickinput/browser/media/quickInput.css @@ -170,7 +170,7 @@ border-top-style: solid; } -.quick-input-list .monaco-list-row:first-child .quick-input-list-entry.quick-input-list-separator-border { +.quick-input-list .monaco-list-row[data-index="0"] .quick-input-list-entry.quick-input-list-separator-border { border-top-style: none; } @@ -276,3 +276,14 @@ .quick-input-list .monaco-list-row.focused .quick-input-list-entry-action-bar .action-label { display: flex; } + +/* focused items in quick pick */ +.quick-input-list .monaco-list-row.focused .monaco-keybinding-key, +.quick-input-list .monaco-list-row.focused .quick-input-list-entry .quick-input-list-separator, +.quick-input-list .monaco-list-row.focused .quick-input-list-rows .quick-input-list-row .codicon, +.quick-input-list .monaco-list-row.focused .quick-input-list-entry-action-bar .codicon { + color: inherit +} +.quick-input-list .monaco-list-row.focused .monaco-keybinding-key { + background: none; +} diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index 44db804e97..4b13dba30b 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/quickInput'; -import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods, IQuickPickAcceptEvent, NO_KEY_MODS, ItemActivation, QuickInputHideReason, IQuickInputHideEvent } from 'vs/base/parts/quickinput/common/quickInput'; +import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods, IQuickPickDidAcceptEvent, NO_KEY_MODS, ItemActivation, QuickInputHideReason, IQuickInputHideEvent, IQuickPickWillAcceptEvent } from 'vs/base/parts/quickinput/common/quickInput'; import * as dom from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { QuickInputList, QuickInputListFocus } from './quickInputList'; @@ -29,7 +29,6 @@ import { IInputBoxStyles } from 'vs/base/browser/ui/inputbox/inputBox'; 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 { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { isString } from 'vs/base/common/types'; import { IKeybindingLabelStyles } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; @@ -359,7 +358,7 @@ class QuickInput extends Disposable implements IQuickInput { const validationMessage = this.validationMessage || this.noValidationMessage; if (this._lastValidationMessage !== validationMessage) { this._lastValidationMessage = validationMessage; - dom.reset(this.ui.message, ...renderLabelWithIcons(escape(validationMessage))); + dom.reset(this.ui.message, ...renderLabelWithIcons(validationMessage)); } if (this._lastSeverity !== this.severity) { this._lastSeverity = this.severity; @@ -428,7 +427,8 @@ class QuickPick extends QuickInput implements IQuickPi private _ariaLabel: string | undefined; private _placeholder: string | undefined; private readonly onDidChangeValueEmitter = this._register(new Emitter()); - private readonly onDidAcceptEmitter = this._register(new Emitter()); + private readonly onWillAcceptEmitter = this._register(new Emitter()); + private readonly onDidAcceptEmitter = this._register(new Emitter()); private readonly onDidCustomEmitter = this._register(new Emitter()); private _items: Array = []; private itemsUpdated = false; @@ -473,8 +473,11 @@ class QuickPick extends QuickInput implements IQuickPi } set value(value: string) { - this._value = value || ''; - this.update(); + if (this._value !== value) { + this._value = value || ''; + this.update(); + this.onDidChangeValueEmitter.fire(this._value); + } } filterValue = (value: string) => value; @@ -499,6 +502,7 @@ class QuickPick extends QuickInput implements IQuickPi onDidChangeValue = this.onDidChangeValueEmitter.event; + onWillAccept = this.onWillAcceptEmitter.event; onDidAccept = this.onDidAcceptEmitter.event; onDidCustom = this.onDidCustomEmitter.event; @@ -761,7 +765,7 @@ class QuickPick extends QuickInput implements IQuickPi if (this.activeItems[0]) { this._selectedItems = [this.activeItems[0]]; this.onDidChangeSelectionEmitter.fire(this.selectedItems); - this.onDidAcceptEmitter.fire({ inBackground: true }); + this.handleAccept(true); } break; @@ -784,7 +788,7 @@ class QuickPick extends QuickInput implements IQuickPi this._selectedItems = [this.activeItems[0]]; this.onDidChangeSelectionEmitter.fire(this.selectedItems); } - this.onDidAcceptEmitter.fire({ inBackground: false }); + this.handleAccept(false); })); this.visibleDisposables.add(this.ui.onDidCustom(() => { this.onDidCustomEmitter.fire(); @@ -812,7 +816,7 @@ class QuickPick extends QuickInput implements IQuickPi this._selectedItems = selectedItems as T[]; this.onDidChangeSelectionEmitter.fire(selectedItems as T[]); if (selectedItems.length) { - this.onDidAcceptEmitter.fire({ inBackground: event instanceof MouseEvent && event.button === 1 /* mouse middle click */ }); + this.handleAccept(event instanceof MouseEvent && event.button === 1 /* mouse middle click */); } })); this.visibleDisposables.add(this.ui.list.onChangedCheckedElements(checkedItems => { @@ -832,6 +836,18 @@ class QuickPick extends QuickInput implements IQuickPi super.show(); // TODO: Why have show() bubble up while update() trickles down? (Could move setComboboxAccessibility() here.) } + private handleAccept(inBackground: boolean): void { + + // Figure out veto via `onWillAccept` event + let veto = false; + this.onWillAcceptEmitter.fire({ veto: () => veto = true }); + + // Continue with `onDidAccpet` if no veto + if (!veto) { + this.onDidAcceptEmitter.fire({ inBackground }); + } + } + private registerQuickNavigation() { return dom.addDisposableListener(this.ui.container, dom.EventType.KEY_UP, e => { if (this.canSelectMany || !this._quickNavigate) { @@ -876,7 +892,7 @@ class QuickPick extends QuickInput implements IQuickPi if (this.activeItems[0]) { this._selectedItems = [this.activeItems[0]]; this.onDidChangeSelectionEmitter.fire(this.selectedItems); - this.onDidAcceptEmitter.fire({ inBackground: false }); + this.handleAccept(false); } // Unset quick navigate after press. It is only valid once // and should not result in any behaviour change afterwards diff --git a/src/vs/base/parts/quickinput/common/quickInput.ts b/src/vs/base/parts/quickinput/common/quickInput.ts index 940b9519ae..6f5458bff4 100644 --- a/src/vs/base/parts/quickinput/common/quickInput.ts +++ b/src/vs/base/parts/quickinput/common/quickInput.ts @@ -207,7 +207,17 @@ export interface IQuickInput extends IDisposable { hide(): void; } -export interface IQuickPickAcceptEvent { +export interface IQuickPickWillAcceptEvent { + + /** + * Allows to disable the default accept handling + * of the picker. If `veto` is called, the picker + * will not trigger the `onDidAccept` event. + */ + veto(): void; +} + +export interface IQuickPickDidAcceptEvent { /** * Signals if the picker item is to be accepted @@ -239,7 +249,8 @@ export interface IQuickPick extends IQuickInput { readonly onDidChangeValue: Event; - readonly onDidAccept: Event; + readonly onWillAccept: Event; + readonly onDidAccept: Event; /** * If enabled, will fire the `onDidAccept` event when diff --git a/src/vs/base/parts/sandbox/common/electronTypes.ts b/src/vs/base/parts/sandbox/common/electronTypes.ts index 484219dc15..c46c742ac4 100644 --- a/src/vs/base/parts/sandbox/common/electronTypes.ts +++ b/src/vs/base/parts/sandbox/common/electronTypes.ts @@ -13,6 +13,10 @@ export interface MessageBoxOptions { + /** + * Content of the message box. + */ + message: string; /** * Can be `"none"`, `"info"`, `"error"`, `"question"` or `"warning"`. On Windows, * `"question"` displays the same icon as `"info"`, unless you set an icon using @@ -34,10 +38,6 @@ export interface MessageBoxOptions { * Title of the message box, some platforms will not show it. */ title?: string; - /** - * Content of the message box. - */ - message: string; /** * Extra information of the message. */ diff --git a/src/vs/base/parts/sandbox/common/sandboxTypes.ts b/src/vs/base/parts/sandbox/common/sandboxTypes.ts index 0fcb57f961..fada4fa3ce 100644 --- a/src/vs/base/parts/sandbox/common/sandboxTypes.ts +++ b/src/vs/base/parts/sandbox/common/sandboxTypes.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IProcessEnvironment } from 'vs/base/common/platform'; @@ -46,7 +46,7 @@ export interface ISandboxConfiguration { zoomLevel?: number; /** - * @deprecated to be removed soon + * Location of V8 code cache. */ - nodeCachedDataDir?: string; + codeCachePath?: string; } diff --git a/src/vs/base/parts/sandbox/electron-browser/preload.js b/src/vs/base/parts/sandbox/electron-browser/preload.js index 8bb6e6854d..f606bbbe75 100644 --- a/src/vs/base/parts/sandbox/electron-browser/preload.js +++ b/src/vs/base/parts/sandbox/electron-browser/preload.js @@ -112,12 +112,6 @@ ipcRenderer.invoke('vscode:fetchShellEnv') ]); - if (!process.env['VSCODE_SKIP_PROCESS_ENV_PATCHING'] /* TODO@bpasero for https://github.com/microsoft/vscode/issues/108804 */) { - // Assign all keys of the shell environment to our process environment - // But make sure that the user environment wins in the end over shell environment - Object.assign(process.env, shellEnv, userEnv); - } - return { ...process.env, ...shellEnv, ...userEnv }; })(); diff --git a/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts b/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts index 373145c3fb..ca750a7b75 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts @@ -36,6 +36,9 @@ export interface IpcRendererEvent extends Event { } export interface IpcRenderer { + + // Docs: https://electronjs.org/docs/api/ipc-renderer + /** * Listens to `channel`, when a new message arrives `listener` would be called with * `listener(event, args...)`. @@ -58,9 +61,13 @@ export interface IpcRenderer { * 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. + * > **NOTE:** Sending non-standard JavaScript types such as DOM objects or special + * Electron objects will throw an exception. + * + * Since the main process does not have support for DOM objects such as + * `ImageBitmap`, `File`, `DOMMatrix` and so on, such objects cannot be sent over + * Electron's IPC to the main process, as the main process would have no way to + * decode them. Attempting to send such objects over IPC will result in an error. * * The main process handles it by listening for `channel` with the `ipcMain` * module. @@ -81,9 +88,13 @@ export interface IpcRenderer { * 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. + * > **NOTE:** Sending non-standard JavaScript types such as DOM objects or special + * Electron objects will throw an exception. + * + * Since the main process does not have support for DOM objects such as + * `ImageBitmap`, `File`, `DOMMatrix` and so on, such objects cannot be sent over + * Electron's IPC to the main process, as the main process would have no way to + * decode them. Attempting to send such objects over IPC will result in an error. * * The main process should listen for `channel` with `ipcMain.handle()`. * @@ -111,7 +122,7 @@ export interface IpcRenderer { // * For more information on using `MessagePort` and `MessageChannel`, see the MDN // * documentation. // */ - // postMessage(channel: string, message: any): void; + // postMessage(channel: string, message: any, transfer?: MessagePort[]): void; } export interface WebFrame { @@ -119,6 +130,11 @@ export interface WebFrame { * Changes the zoom level to the specified level. The original size is 0 and each * increment above or below represents zooming 20% larger or smaller to default * limits of 300% and 50% of original size, respectively. + * + * > **NOTE**: The zoom policy at the Chromium level is same-origin, meaning that + * the zoom level for a specific domain propagates across all instances of windows + * with the same domain. Differentiating the window URLs will make zoom work + * per-window. */ setZoomLevel(level: number): void; } @@ -207,7 +223,7 @@ export interface CrashReporterStartOptions { rateLimit?: boolean; /** * If true, crash reports will be compressed and uploaded with `Content-Encoding: - * gzip`. Default is `false`. + * gzip`. Default is `true`. */ compress?: boolean; /** diff --git a/src/vs/base/parts/storage/node/storage.ts b/src/vs/base/parts/storage/node/storage.ts index 5443934e85..6f25d56a7f 100644 --- a/src/vs/base/parts/storage/node/storage.ts +++ b/src/vs/base/parts/storage/node/storage.ts @@ -4,12 +4,11 @@ *--------------------------------------------------------------------------------------------*/ 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 } from 'vs/base/node/pfs'; +import { Promises } from 'vs/base/node/pfs'; import { IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage'; interface IDatabaseConnection { @@ -187,7 +186,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 promises.unlink(this.path).then(() => { + return Promises.unlink(this.path).then(() => { // Re-open the DB fresh return this.doConnect(this.path).then(recoveryConnection => { @@ -217,7 +216,7 @@ export class SQLiteStorageDatabase implements IStorageDatabase { private backup(): Promise { const backupPath = this.toBackupPath(this.path); - return copy(this.path, backupPath, { preserveSymlinks: false }); + return Promises.copy(this.path, backupPath, { preserveSymlinks: false }); } private toBackupPath(path: string): string { @@ -273,9 +272,9 @@ export class SQLiteStorageDatabase implements IStorageDatabase { // folder is really not writeable for us. // try { - await promises.unlink(path); + await Promises.unlink(path); try { - await promises.rename(this.toBackupPath(path), path); + await Promises.rename(this.toBackupPath(path), path); } catch (error) { // ignore } 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 2cd019c1fa..6ae2aef7b0 100644 --- a/src/vs/base/parts/storage/test/node/storage.test.ts +++ b/src/vs/base/parts/storage/test/node/storage.test.ts @@ -7,9 +7,8 @@ import { SQLiteStorageDatabase, ISQLiteStorageDatabaseOptions } from 'vs/base/pa import { Storage, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/base/parts/storage/common/storage'; import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; -import { promises } from 'fs'; import { strictEqual, ok } from 'assert'; -import { writeFile, exists, rimraf } from 'vs/base/node/pfs'; +import { Promises } 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'; @@ -23,11 +22,11 @@ flakySuite('Storage Library', function () { setup(function () { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storagelibrary'); - return promises.mkdir(testDir, { recursive: true }); + return Promises.mkdir(testDir, { recursive: true }); }); teardown(function () { - return rimraf(testDir); + return Promises.rm(testDir); }); test('basics', async () => { @@ -214,7 +213,7 @@ flakySuite('Storage Library', function () { }); test.skip('conflicting updates', async () => { // {{SQL CARBON EDIT}} test is disabled due to failures - let storage = new Storage(new SQLiteStorageDatabase(join('storageDir', 'storage.db'))); + let storage = new Storage(new SQLiteStorageDatabase(join(testDir, 'storage.db'))); await storage.init(); let changes = new Set(); @@ -263,7 +262,7 @@ flakySuite('Storage Library', function () { await storage.set('bar', 'foo'); - await writeFile(storageFile, 'This is a broken DB'); + await Promises.writeFile(storageFile, 'This is a broken DB'); await storage.set('foo', 'bar'); @@ -296,11 +295,11 @@ flakySuite('SQLite Storage Library', function () { setup(function () { testdir = getRandomTestPath(tmpdir(), 'vsctests', 'storagelibrary'); - return promises.mkdir(testdir, { recursive: true }); + return Promises.mkdir(testdir, { recursive: true }); }); teardown(function () { - return rimraf(testdir); + return Promises.rm(testdir); }); async function testDBBasics(path: string, logError?: (error: Error | string) => void) { @@ -386,7 +385,7 @@ flakySuite('SQLite Storage Library', function () { test('basics (corrupt DB falls back to empty DB)', async () => { const corruptDBPath = join(testdir, 'broken.db'); - await writeFile(corruptDBPath, 'This is a broken DB'); + await Promises.writeFile(corruptDBPath, 'This is a broken DB'); let expectedError: any; await testDBBasics(corruptDBPath, error => { @@ -408,7 +407,7 @@ flakySuite('SQLite Storage Library', function () { await storage.updateItems({ insert: items }); await storage.close(); - await writeFile(storagePath, 'This is now a broken DB'); + await Promises.writeFile(storagePath, 'This is now a broken DB'); storage = new SQLiteStorageDatabase(storagePath); @@ -440,8 +439,8 @@ flakySuite('SQLite Storage Library', function () { await storage.updateItems({ insert: items }); await storage.close(); - await writeFile(storagePath, 'This is now a broken DB'); - await writeFile(`${storagePath}.backup`, 'This is now also a broken DB'); + await Promises.writeFile(storagePath, 'This is now a broken DB'); + await Promises.writeFile(`${storagePath}.backup`, 'This is now also a broken DB'); storage = new SQLiteStorageDatabase(storagePath); @@ -464,12 +463,12 @@ flakySuite('SQLite Storage Library', function () { await storage.close(); const backupPath = `${storagePath}.backup`; - strictEqual(await exists(backupPath), true); + strictEqual(await Promises.exists(backupPath), true); storage = new SQLiteStorageDatabase(storagePath); await storage.getItems(); - await writeFile(storagePath, 'This is now a broken DB'); + await Promises.writeFile(storagePath, 'This is now a broken DB'); // we still need to trigger a check to the DB so that we get to know that // the DB is corrupt. We have no extra code on shutdown that checks for the @@ -477,7 +476,7 @@ flakySuite('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 promises.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(() => { @@ -487,7 +486,7 @@ flakySuite('SQLite Storage Library', function () { }); strictEqual(recoveryCalled, true); - strictEqual(await exists(backupPath), true); + strictEqual(await Promises.exists(backupPath), true); storage = new SQLiteStorageDatabase(storagePath); diff --git a/src/vs/base/parts/tree/browser/treeDefaults.ts b/src/vs/base/parts/tree/browser/treeDefaults.ts index aa5ad3d70d..b3d459c3e5 100644 --- a/src/vs/base/parts/tree/browser/treeDefaults.ts +++ b/src/vs/base/parts/tree/browser/treeDefaults.ts @@ -218,7 +218,7 @@ export class DefaultController implements _.IController { protected isClickOnTwistie(event: mouse.IMouseEvent): boolean { let element = event.target as HTMLElement; - if (!dom.hasClass(element, 'content')) { + if (!element.classList.contains('content')) { return false; } diff --git a/src/vs/base/parts/tree/browser/treeView.ts b/src/vs/base/parts/tree/browser/treeView.ts index 50d44ddf10..4cc574e9ed 100644 --- a/src/vs/base/parts/tree/browser/treeView.ts +++ b/src/vs/base/parts/tree/browser/treeView.ts @@ -493,11 +493,11 @@ export class TreeView extends HeightMap { } if (this.context.options.alwaysFocused) { - DOM.addClass(this.domNode, 'focused'); + this.domNode.classList.add('focused'); } if (!this.context.options.paddingOnRow) { - DOM.addClass(this.domNode, 'no-row-padding'); + this.domNode.classList.add('no-row-padding'); } this.wrapper = document.createElement('div'); @@ -1023,7 +1023,7 @@ export class TreeView extends HeightMap { viewItem.addClass(trait); } if (trait === 'highlighted') { - DOM.addClass(this.domNode, trait); + this.domNode.classList.add(trait); // Ugly Firefox fix: input fields can't be selected if parent nodes are draggable if (viewItem) { @@ -1043,7 +1043,7 @@ export class TreeView extends HeightMap { viewItem.removeClass(trait); } if (trait === 'highlighted') { - DOM.removeClass(this.domNode, trait); + this.domNode.classList.remove(trait); // Ugly Firefox fix: input fields can't be selected if parent nodes are draggable if (this.highlightedItemWasDraggable) { @@ -1056,7 +1056,7 @@ export class TreeView extends HeightMap { private onModelFocusChange(): void { const focus = this.model && this.model.getFocus(); - DOM.toggleClass(this.domNode, 'no-focused-item', !focus); + this.domNode.classList.toggle('no-focused-item', !focus); // ARIA if (focus) { @@ -1512,7 +1512,7 @@ export class TreeView extends HeightMap { private onFocus(): void { if (!this.context.options.alwaysFocused) { - DOM.addClass(this.domNode, 'focused'); + this.domNode.classList.add('focused'); } this._onDOMFocus.fire(); @@ -1520,7 +1520,7 @@ export class TreeView extends HeightMap { private onBlur(): void { if (!this.context.options.alwaysFocused) { - DOM.removeClass(this.domNode, 'focused'); + this.domNode.classList.remove('focused'); } this.domNode.removeAttribute('aria-activedescendant'); // ARIA diff --git a/src/vs/base/test/browser/comparers.test.ts b/src/vs/base/test/browser/comparers.test.ts index 3576863d25..539f2e2e1c 100644 --- a/src/vs/base/test/browser/comparers.test.ts +++ b/src/vs/base/test/browser/comparers.test.ts @@ -3,13 +3,23 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { compareFileNames, compareFileExtensions, compareFileNamesDefault, compareFileExtensionsDefault } from 'vs/base/common/comparers'; +import { + compareFileNames, + compareFileExtensions, + compareFileNamesDefault, + compareFileExtensionsDefault, + compareFileNamesUpper, + compareFileExtensionsUpper, + compareFileNamesLower, + compareFileExtensionsLower, + compareFileNamesUnicode, + compareFileExtensionsUnicode, +} from 'vs/base/common/comparers'; import * as assert from 'assert'; const compareLocale = (a: string, b: string) => a.localeCompare(b); const compareLocaleNumeric = (a: string, b: string) => a.localeCompare(b, undefined, { numeric: true }); - suite('Comparers', () => { test('compareFileNames', () => { @@ -23,11 +33,11 @@ suite('Comparers', () => { assert(compareFileNames(null, 'abc') < 0, 'null should be come before real values'); assert(compareFileNames('', '') === 0, 'empty should be equal'); assert(compareFileNames('abc', 'abc') === 0, 'equal names should be equal'); - assert(compareFileNames('z', 'A') > 0, 'z comes is after A regardless of case'); - assert(compareFileNames('Z', 'a') > 0, 'Z comes after a regardless of case'); + assert(compareFileNames('z', 'A') > 0, 'z comes after A'); + assert(compareFileNames('Z', 'a') > 0, 'Z comes after a'); // name plus extension comparisons - assert(compareFileNames('bbb.aaa', 'aaa.bbb') > 0, 'files with extensions are compared first by filename'); + assert(compareFileNames('bbb.aaa', 'aaa.bbb') > 0, 'compares the whole name all at once by locale'); assert(compareFileNames('aggregate.go', 'aggregate_repo.go') > 0, 'compares the whole name all at once by locale'); // dotfile comparisons @@ -35,7 +45,7 @@ suite('Comparers', () => { assert(compareFileNames('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly'); assert(compareFileNames('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); assert(compareFileNames('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); - assert(compareFileNames('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot'); + assert(compareFileNames('.aaa_env', '.aaa.env') < 0, 'an underscore in a dotfile name will sort before a dot'); // dotfile vs non-dotfile comparisons assert(compareFileNames(null, '.abc') < 0, 'null should come before dotfiles'); @@ -51,14 +61,16 @@ suite('Comparers', () => { assert(compareFileNames('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long'); assert(compareFileNames('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); assert(compareFileNames('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + assert(compareFileNames('a.ext1', 'b.Ext1') < 0, 'if names are different and extensions with numbers are equal except for case, filenames are sorted in name order'); + assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileNames), ['A2.txt', 'a10.txt', 'a20.txt', 'A100.txt'], 'filenames with number and case differences compare numerically'); // // Comparisons with different results than compareFileNamesDefault // // name-only comparisons - assert(compareFileNames('a', 'A') !== compareLocale('a', 'A'), 'the same letter does not sort by locale'); - assert(compareFileNames('â', 'Â') !== compareLocale('â', 'Â'), 'the same accented letter does not sort by locale'); + assert(compareFileNames('a', 'A') !== compareLocale('a', 'A'), 'the same letter sorts in unicode order, not by locale'); + assert(compareFileNames('â', 'Â') !== compareLocale('â', 'Â'), 'the same accented letter sorts in unicode order, not by locale'); assert.notDeepStrictEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNames), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases do not sort in locale order'); assert.notDeepStrictEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileNames), ['email', 'Email', 'émail', 'Émail'].sort(compareLocale), 'the same base characters with different case or accents do not sort in locale order'); @@ -67,6 +79,7 @@ suite('Comparers', () => { assert(compareFileNames('abc.txt1', 'abc.txt01') > 0, 'same name plus extensions with equal numbers sort in unicode order'); assert(compareFileNames('art01', 'Art01') !== 'art01'.localeCompare('Art01', undefined, { numeric: true }), 'a numerically equivalent word of a different case does not compare numerically based on locale'); + assert(compareFileNames('a.ext1', 'a.Ext1') > 0, 'if names are equal and extensions with numbers are equal except for case, filenames are sorted in full filename unicode order'); }); @@ -89,9 +102,6 @@ suite('Comparers', () => { assert(compareFileExtensions('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); assert(compareFileExtensions('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); assert(compareFileExtensions('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extensions even if filenames compare differently'); - assert(compareFileExtensions('agg.go', 'aggrepo.go') < 0, 'shorter names sort before longer names'); - assert(compareFileExtensions('agg.go', 'agg_repo.go') < 0, 'shorter names short before longer names even when the longer name contains an underscore'); - assert(compareFileExtensions('a.MD', 'b.md') < 0, 'when extensions are the same except for case, the files sort by name'); // dotfile comparisons assert(compareFileExtensions('.abc', '.abc') === 0, 'equal dotfiles should be equal'); @@ -113,8 +123,8 @@ suite('Comparers', () => { assert(compareFileExtensions('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal'); assert(compareFileExtensions('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); assert(compareFileExtensions('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long'); - assert(compareFileExtensions('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared'); - assert(compareFileExtensions('a10.txt', 'A2.txt') > 0, 'filenames with number and case differences compare numerically'); + assert(compareFileExtensions('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, names should be compared'); + assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileExtensions), ['A2.txt', 'a10.txt', 'a20.txt', 'A100.txt'], 'filenames with number and case differences compare numerically'); // // Comparisons with different results from compareFileExtensionsDefault @@ -127,8 +137,9 @@ suite('Comparers', () => { 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'); - assert(compareFileExtensions('a.md', 'A.md') !== compareLocale('a', 'A'), 'case differences in names do not sort by locale'); + assert(compareFileExtensions('a.MD', 'a.md') < 0, 'case differences in extensions sort in unicode order'); + assert(compareFileExtensions('a.md', 'A.md') > 0, 'case differences in names sort in unicode order'); + assert(compareFileExtensions('a.md', 'b.MD') > 0, 'when extensions are the same except for case, the files sort by extension'); assert(compareFileExtensions('aggregate.go', 'aggregate_repo.go') < 0, 'when extensions are equal, names sort in dictionary order'); // dotfile comparisons @@ -144,6 +155,8 @@ suite('Comparers', () => { assert(compareFileExtensions('art01', 'Art01') !== compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case does not compare by locale'); assert(compareFileExtensions('abc02.txt', 'abc002.txt') > 0, 'filenames with equivalent numbers and leading zeros sort in unicode order'); assert(compareFileExtensions('txt.abc01', 'txt.abc1') < 0, 'extensions with equivalent numbers sort in unicode order'); + assert(compareFileExtensions('a.ext1', 'b.Ext1') > 0, 'if names are different and extensions with numbers are equal except for case, filenames are sorted in extension unicode order'); + assert(compareFileExtensions('a.ext1', 'a.Ext1') > 0, 'if names are equal and extensions with numbers are equal except for case, filenames are sorted in extension unicode order'); }); @@ -158,8 +171,8 @@ suite('Comparers', () => { assert(compareFileNamesDefault(null, 'abc') < 0, 'null should be come before real values'); assert(compareFileNamesDefault('', '') === 0, 'empty should be equal'); assert(compareFileNamesDefault('abc', 'abc') === 0, 'equal names should be equal'); - assert(compareFileNamesDefault('z', 'A') > 0, 'z comes is after A regardless of case'); - assert(compareFileNamesDefault('Z', 'a') > 0, 'Z comes after a regardless of case'); + assert(compareFileNamesDefault('z', 'A') > 0, 'z comes after A'); + assert(compareFileNamesDefault('Z', 'a') > 0, 'Z comes after a'); // name plus extension comparisons assert(compareFileNamesDefault('file.ext', 'file.ext') === 0, 'equal full names should be equal'); @@ -173,7 +186,7 @@ suite('Comparers', () => { assert(compareFileNamesDefault('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly'); assert(compareFileNamesDefault('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); assert(compareFileNamesDefault('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); - assert(compareFileNamesDefault('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot'); + assert(compareFileNamesDefault('.aaa_env', '.aaa.env') < 0, 'an underscore in a dotfile name will sort before a dot'); // dotfile vs non-dotfile comparisons assert(compareFileNamesDefault(null, '.abc') < 0, 'null should come before dotfiles'); @@ -189,6 +202,8 @@ suite('Comparers', () => { assert(compareFileNamesDefault('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long'); assert(compareFileNamesDefault('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); assert(compareFileNamesDefault('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + assert(compareFileNamesDefault('a.ext1', 'b.Ext1') < 0, 'if names are different and extensions with numbers are equal except for case, filenames are compared by full filename'); + assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileNamesDefault), ['A2.txt', 'a10.txt', 'a20.txt', 'A100.txt'], 'filenames with number and case differences compare numerically'); // // Comparisons with different results than compareFileNames @@ -203,7 +218,7 @@ suite('Comparers', () => { assert(compareFileNamesDefault('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest number first'); assert(compareFileNamesDefault('abc.txt1', 'abc.txt01') < 0, 'same name plus extensions with equal numbers sort shortest number first'); assert(compareFileNamesDefault('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale'); - + assert(compareFileNamesDefault('a.ext1', 'a.Ext1') === compareLocale('ext1', 'Ext1'), 'if names are equal and extensions with numbers are equal except for case, filenames are sorted in extension locale order'); }); test('compareFileExtensionsDefault', () => { @@ -225,8 +240,6 @@ suite('Comparers', () => { assert(compareFileExtensionsDefault('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); assert(compareFileExtensionsDefault('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); assert(compareFileExtensionsDefault('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first'); - assert(compareFileExtensionsDefault('agg.go', 'aggrepo.go') < 0, 'shorter names sort before longer names'); - assert(compareFileExtensionsDefault('a.MD', 'b.md') < 0, 'when extensions are the same except for case, the files sort by name'); // dotfile comparisons assert(compareFileExtensionsDefault('.abc', '.abc') === 0, 'equal dotfiles should be equal'); @@ -248,8 +261,8 @@ suite('Comparers', () => { assert(compareFileExtensionsDefault('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal'); assert(compareFileExtensionsDefault('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); assert(compareFileExtensionsDefault('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long'); - assert(compareFileExtensionsDefault('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared'); - assert(compareFileExtensionsDefault('a10.txt', 'A2.txt') > 0, 'filenames with number and case differences compare numerically'); + assert(compareFileExtensionsDefault('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, full filenames should be compared'); + assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileExtensionsDefault), ['A2.txt', 'a10.txt', 'a20.txt', 'A100.txt'], 'filenames with number and case differences compare numerically'); // // Comparisons with different results than compareFileExtensions @@ -263,6 +276,7 @@ suite('Comparers', () => { // name plus extension comparisons assert(compareFileExtensionsDefault('a.MD', 'a.md') === compareLocale('MD', 'md'), 'case differences in extensions sort by locale'); assert(compareFileExtensionsDefault('a.md', 'A.md') === compareLocale('a', 'A'), 'case differences in names sort by locale'); + assert(compareFileExtensionsDefault('a.md', 'b.MD') < 0, 'when extensions are the same except for case, the files sort by name'); assert(compareFileExtensionsDefault('aggregate.go', 'aggregate_repo.go') > 0, 'names with the same extension sort in full filename locale order'); // dotfile comparisons @@ -278,6 +292,418 @@ suite('Comparers', () => { assert(compareFileExtensionsDefault('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale'); assert(compareFileExtensionsDefault('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest string first'); assert(compareFileExtensionsDefault('txt.abc01', 'txt.abc1') > 0, 'extensions with equivalent numbers sort shortest extension first'); + assert(compareFileExtensionsDefault('a.ext1', 'b.Ext1') < 0, 'if extensions with numbers are equal except for case, full filenames should be compared'); + assert(compareFileExtensionsDefault('a.ext1', 'a.Ext1') === compareLocale('a.ext1', 'a.Ext1'), 'if extensions with numbers are equal except for case, full filenames are compared in locale order'); }); + + test('compareFileNamesUpper', () => { + + // + // Comparisons with the same results as compareFileNamesDefault + // + + // name-only comparisons + assert(compareFileNamesUpper(null, null) === 0, 'null should be equal'); + assert(compareFileNamesUpper(null, 'abc') < 0, 'null should be come before real values'); + assert(compareFileNamesUpper('', '') === 0, 'empty should be equal'); + assert(compareFileNamesUpper('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileNamesUpper('z', 'A') > 0, 'z comes after A'); + + // name plus extension comparisons + assert(compareFileNamesUpper('file.ext', 'file.ext') === 0, 'equal full names should be equal'); + assert(compareFileNamesUpper('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileNamesUpper('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); + assert(compareFileNamesUpper('bbb.aaa', 'aaa.bbb') > 0, 'files should be compared by names even if extensions compare differently'); + assert(compareFileNamesUpper('aggregate.go', 'aggregate_repo.go') > 0, 'compares the full filename in locale order'); + + // dotfile comparisons + assert(compareFileNamesUpper('.abc', '.abc') === 0, 'equal dotfile names should be equal'); + assert(compareFileNamesUpper('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly'); + assert(compareFileNamesUpper('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileNamesUpper('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + assert(compareFileNamesUpper('.aaa_env', '.aaa.env') < 0, 'an underscore in a dotfile name will sort before a dot'); + + // dotfile vs non-dotfile comparisons + assert(compareFileNamesUpper(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileNamesUpper('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileNamesUpper('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileNamesUpper('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + assert(compareFileNamesUpper('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); + + // numeric comparisons + assert(compareFileNamesUpper('1', '1') === 0, 'numerically equal full names should be equal'); + assert(compareFileNamesUpper('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); + assert(compareFileNamesUpper('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); + assert(compareFileNamesUpper('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long'); + assert(compareFileNamesUpper('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileNamesUpper('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + assert(compareFileNamesUpper('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest number first'); + assert(compareFileNamesUpper('abc.txt1', 'abc.txt01') < 0, 'same name plus extensions with equal numbers sort shortest number first'); + assert(compareFileNamesUpper('a.ext1', 'b.Ext1') < 0, 'different names with the equal extensions except for case are sorted by full filename'); + assert(compareFileNamesUpper('a.ext1', 'a.Ext1') === compareLocale('a.ext1', 'a.Ext1'), 'same names with equal and extensions except for case are sorted in full filename locale order'); + + // + // Comparisons with different results than compareFileNamesDefault + // + + // name-only comparisons + assert(compareFileNamesUpper('Z', 'a') < 0, 'Z comes before a'); + assert(compareFileNamesUpper('a', 'A') > 0, 'the same letter sorts uppercase first'); + assert(compareFileNamesUpper('â', 'Â') > 0, 'the same accented letter sorts uppercase first'); + assert.deepStrictEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNamesUpper), ['Art', 'Artichoke', 'art', 'artichoke'], 'names with the same root and different cases sort uppercase first'); + assert.deepStrictEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileNamesUpper), ['Email', 'Émail', 'email', 'émail'], 'the same base characters with different case or accents sort uppercase first'); + + // numeric comparisons + assert(compareFileNamesUpper('art01', 'Art01') > 0, 'a numerically equivalent name of a different case compares uppercase first'); + assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileNamesUpper), ['A2.txt', 'A100.txt', 'a10.txt', 'a20.txt'], 'filenames with number and case differences group by case then compare by number'); + + }); + + test('compareFileExtensionsUpper', () => { + + // + // Comparisons with the same result as compareFileExtensionsDefault + // + + // name-only comparisons + assert(compareFileExtensionsUpper(null, null) === 0, 'null should be equal'); + assert(compareFileExtensionsUpper(null, 'abc') < 0, 'null should come before real files without extensions'); + assert(compareFileExtensionsUpper('', '') === 0, 'empty should be equal'); + assert(compareFileExtensionsUpper('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileExtensionsUpper('z', 'A') > 0, 'z comes after A'); + + // name plus extension comparisons + assert(compareFileExtensionsUpper('file.ext', 'file.ext') === 0, 'equal full filenames should be equal'); + assert(compareFileExtensionsUpper('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileExtensionsUpper('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); + assert(compareFileExtensionsUpper('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first'); + assert(compareFileExtensionsUpper('a.md', 'b.MD') < 0, 'when extensions are the same except for case, the files sort by name'); + assert(compareFileExtensionsUpper('a.MD', 'a.md') === compareLocale('MD', 'md'), 'case differences in extensions sort by locale'); + assert(compareFileExtensionsUpper('aggregate.go', 'aggregate_repo.go') > 0, 'when extensions are equal, compares the full filename'); + + // dotfile comparisons + assert(compareFileExtensionsUpper('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileExtensionsUpper('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case'); + assert(compareFileExtensionsUpper('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileExtensionsUpper('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + + // dotfile vs non-dotfile comparisons + assert(compareFileExtensionsUpper(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileExtensionsUpper('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileExtensionsUpper('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); + assert(compareFileExtensionsUpper('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileExtensionsUpper('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + + // numeric comparisons + assert(compareFileExtensionsUpper('1', '1') === 0, 'numerically equal full names should be equal'); + assert(compareFileExtensionsUpper('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); + assert(compareFileExtensionsUpper('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsUpper('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order'); + assert(compareFileExtensionsUpper('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileExtensionsUpper('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + assert(compareFileExtensionsUpper('abc2.txt2', 'abc1.txt10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsUpper('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal'); + assert(compareFileExtensionsUpper('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsUpper('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long'); + assert(compareFileExtensionsUpper('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, full filenames should be compared'); + assert(compareFileExtensionsUpper('abc.txt01', 'abc.txt1') > 0, 'extensions with equal numbers should be in shortest-first order'); + assert(compareFileExtensionsUpper('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest string first'); + assert(compareFileExtensionsUpper('txt.abc01', 'txt.abc1') > 0, 'extensions with equivalent numbers sort shortest extension first'); + assert(compareFileExtensionsUpper('a.ext1', 'b.Ext1') < 0, 'different names and extensions that are equal except for case are sorted in full filename order'); + assert(compareFileExtensionsUpper('a.ext1', 'a.Ext1') === compareLocale('a.ext1', 'b.Ext1'), 'same names and extensions that are equal except for case are sorted in full filename locale order'); + + // + // Comparisons with different results than compareFileExtensionsDefault + // + + // name-only comparisons + assert(compareFileExtensionsUpper('Z', 'a') < 0, 'Z comes before a'); + assert(compareFileExtensionsUpper('a', 'A') > 0, 'the same letter sorts uppercase first'); + assert(compareFileExtensionsUpper('â', 'Â') > 0, 'the same accented letter sorts uppercase first'); + assert.deepStrictEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensionsUpper), ['Art', 'Artichoke', 'art', 'artichoke'], 'names with the same root and different cases sort uppercase names first'); + assert.deepStrictEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileExtensionsUpper), ['Email', 'Émail', 'email', 'émail'], 'the same base characters with different case or accents sort uppercase names first'); + + // name plus extension comparisons + assert(compareFileExtensionsUpper('a.md', 'A.md') > 0, 'case differences in names sort uppercase first'); + assert(compareFileExtensionsUpper('art01', 'Art01') > 0, 'a numerically equivalent word of a different case sorts uppercase first'); + assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileExtensionsUpper), ['A2.txt', 'A100.txt', 'a10.txt', 'a20.txt',], 'filenames with number and case differences group by case then sort by number'); + + }); + + test('compareFileNamesLower', () => { + + // + // Comparisons with the same results as compareFileNamesDefault + // + + // name-only comparisons + assert(compareFileNamesLower(null, null) === 0, 'null should be equal'); + assert(compareFileNamesLower(null, 'abc') < 0, 'null should be come before real values'); + assert(compareFileNamesLower('', '') === 0, 'empty should be equal'); + assert(compareFileNamesLower('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileNamesLower('Z', 'a') > 0, 'Z comes after a'); + + // name plus extension comparisons + assert(compareFileNamesLower('file.ext', 'file.ext') === 0, 'equal full names should be equal'); + assert(compareFileNamesLower('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileNamesLower('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); + assert(compareFileNamesLower('bbb.aaa', 'aaa.bbb') > 0, 'files should be compared by names even if extensions compare differently'); + assert(compareFileNamesLower('aggregate.go', 'aggregate_repo.go') > 0, 'compares full filenames'); + + // dotfile comparisons + assert(compareFileNamesLower('.abc', '.abc') === 0, 'equal dotfile names should be equal'); + assert(compareFileNamesLower('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly'); + assert(compareFileNamesLower('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileNamesLower('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + assert(compareFileNamesLower('.aaa_env', '.aaa.env') < 0, 'an underscore in a dotfile name will sort before a dot'); + + // dotfile vs non-dotfile comparisons + assert(compareFileNamesLower(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileNamesLower('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileNamesLower('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileNamesLower('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + assert(compareFileNamesLower('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); + + // numeric comparisons + assert(compareFileNamesLower('1', '1') === 0, 'numerically equal full names should be equal'); + assert(compareFileNamesLower('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); + assert(compareFileNamesLower('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); + assert(compareFileNamesLower('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long'); + assert(compareFileNamesLower('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileNamesLower('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + assert(compareFileNamesLower('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest number first'); + assert(compareFileNamesLower('abc.txt1', 'abc.txt01') < 0, 'same name plus extensions with equal numbers sort shortest number first'); + assert(compareFileNamesLower('a.ext1', 'b.Ext1') < 0, 'different names and extensions that are equal except for case are sorted in full filename order'); + assert(compareFileNamesLower('a.ext1', 'a.Ext1') === compareLocale('a.ext1', 'b.Ext1'), 'same names and extensions that are equal except for case are sorted in full filename locale order'); + + // + // Comparisons with different results than compareFileNamesDefault + // + + // name-only comparisons + assert(compareFileNamesLower('z', 'A') < 0, 'z comes before A'); + assert(compareFileNamesLower('a', 'A') < 0, 'the same letter sorts lowercase first'); + assert(compareFileNamesLower('â', 'Â') < 0, 'the same accented letter sorts lowercase first'); + assert.deepStrictEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNamesLower), ['art', 'artichoke', 'Art', 'Artichoke'], 'names with the same root and different cases sort lowercase first'); + assert.deepStrictEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileNamesLower), ['email', 'émail', 'Email', 'Émail'], 'the same base characters with different case or accents sort lowercase first'); + + // numeric comparisons + assert(compareFileNamesLower('art01', 'Art01') < 0, 'a numerically equivalent name of a different case compares lowercase first'); + assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileNamesLower), ['a10.txt', 'a20.txt', 'A2.txt', 'A100.txt'], 'filenames with number and case differences group by case then compare by number'); + + }); + + test('compareFileExtensionsLower', () => { + + // + // Comparisons with the same result as compareFileExtensionsDefault + // + + // name-only comparisons + assert(compareFileExtensionsLower(null, null) === 0, 'null should be equal'); + assert(compareFileExtensionsLower(null, 'abc') < 0, 'null should come before real files without extensions'); + assert(compareFileExtensionsLower('', '') === 0, 'empty should be equal'); + assert(compareFileExtensionsLower('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileExtensionsLower('Z', 'a') > 0, 'Z comes after a'); + + // name plus extension comparisons + assert(compareFileExtensionsLower('file.ext', 'file.ext') === 0, 'equal full filenames should be equal'); + assert(compareFileExtensionsLower('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileExtensionsLower('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); + assert(compareFileExtensionsLower('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first'); + assert(compareFileExtensionsLower('a.md', 'b.MD') < 0, 'when extensions are the same except for case, the files sort by name'); + assert(compareFileExtensionsLower('a.MD', 'a.md') === compareLocale('MD', 'md'), 'case differences in extensions sort by locale'); + + // dotfile comparisons + assert(compareFileExtensionsLower('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileExtensionsLower('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case'); + assert(compareFileExtensionsLower('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileExtensionsLower('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + + // dotfile vs non-dotfile comparisons + assert(compareFileExtensionsLower(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileExtensionsLower('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileExtensionsLower('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); + assert(compareFileExtensionsLower('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileExtensionsLower('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + + // numeric comparisons + assert(compareFileExtensionsLower('1', '1') === 0, 'numerically equal full names should be equal'); + assert(compareFileExtensionsLower('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); + assert(compareFileExtensionsLower('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsLower('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order'); + assert(compareFileExtensionsLower('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileExtensionsLower('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + assert(compareFileExtensionsLower('abc2.txt2', 'abc1.txt10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsLower('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal'); + assert(compareFileExtensionsLower('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsLower('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long'); + assert(compareFileExtensionsLower('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, full filenames should be compared'); + assert(compareFileExtensionsLower('abc.txt01', 'abc.txt1') > 0, 'extensions with equal numbers should be in shortest-first order'); + assert(compareFileExtensionsLower('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest string first'); + assert(compareFileExtensionsLower('txt.abc01', 'txt.abc1') > 0, 'extensions with equivalent numbers sort shortest extension first'); + assert(compareFileExtensionsLower('a.ext1', 'b.Ext1') < 0, 'if extensions with numbers are equal except for case, full filenames should be compared'); + assert(compareFileExtensionsLower('a.ext1', 'a.Ext1') === compareLocale('a.ext1', 'a.Ext1'), 'if extensions with numbers are equal except for case, filenames are sorted in locale order'); + + // + // Comparisons with different results than compareFileExtensionsDefault + // + + // name-only comparisons + assert(compareFileExtensionsLower('z', 'A') < 0, 'z comes before A'); + assert(compareFileExtensionsLower('a', 'A') < 0, 'the same letter sorts lowercase first'); + assert(compareFileExtensionsLower('â', 'Â') < 0, 'the same accented letter sorts lowercase first'); + assert.deepStrictEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensionsLower), ['art', 'artichoke', 'Art', 'Artichoke'], 'names with the same root and different cases sort lowercase names first'); + assert.deepStrictEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileExtensionsLower), ['email', 'émail', 'Email', 'Émail'], 'the same base characters with different case or accents sort lowercase names first'); + + // name plus extension comparisons + assert(compareFileExtensionsLower('a.md', 'A.md') < 0, 'case differences in names sort lowercase first'); + assert(compareFileExtensionsLower('art01', 'Art01') < 0, 'a numerically equivalent word of a different case sorts lowercase first'); + assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileExtensionsLower), ['a10.txt', 'a20.txt', 'A2.txt', 'A100.txt'], 'filenames with number and case differences group by case then sort by number'); + assert(compareFileExtensionsLower('aggregate.go', 'aggregate_repo.go') > 0, 'when extensions are equal, compares full filenames'); + + }); + + test('compareFileNamesUnicode', () => { + + // + // Comparisons with the same results as compareFileNamesDefault + // + + // name-only comparisons + assert(compareFileNamesUnicode(null, null) === 0, 'null should be equal'); + assert(compareFileNamesUnicode(null, 'abc') < 0, 'null should be come before real values'); + assert(compareFileNamesUnicode('', '') === 0, 'empty should be equal'); + assert(compareFileNamesUnicode('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileNamesUnicode('z', 'A') > 0, 'z comes after A'); + + // name plus extension comparisons + assert(compareFileNamesUnicode('file.ext', 'file.ext') === 0, 'equal full names should be equal'); + assert(compareFileNamesUnicode('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileNamesUnicode('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); + assert(compareFileNamesUnicode('bbb.aaa', 'aaa.bbb') > 0, 'files should be compared by names even if extensions compare differently'); + + // dotfile comparisons + assert(compareFileNamesUnicode('.abc', '.abc') === 0, 'equal dotfile names should be equal'); + assert(compareFileNamesUnicode('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly'); + assert(compareFileNamesUnicode('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileNamesUnicode('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + + // dotfile vs non-dotfile comparisons + assert(compareFileNamesUnicode(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileNamesUnicode('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileNamesUnicode('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileNamesUnicode('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + assert(compareFileNamesUnicode('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); + + // numeric comparisons + assert(compareFileNamesUnicode('1', '1') === 0, 'numerically equal full names should be equal'); + assert(compareFileNamesUnicode('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); + assert(compareFileNamesUnicode('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); + assert(compareFileNamesUnicode('a.ext1', 'b.Ext1') < 0, 'if names are different and extensions with numbers are equal except for case, filenames are sorted by unicode full filename'); + assert(compareFileNamesUnicode('a.ext1', 'a.Ext1') > 0, 'if names are equal and extensions with numbers are equal except for case, filenames are sorted by unicode full filename'); + + // + // Comparisons with different results than compareFileNamesDefault + // + + // name-only comparisons + assert(compareFileNamesUnicode('Z', 'a') < 0, 'Z comes before a'); + assert(compareFileNamesUnicode('a', 'A') > 0, 'the same letter sorts uppercase first'); + assert(compareFileNamesUnicode('â', 'Â') > 0, 'the same accented letter sorts uppercase first'); + assert.deepStrictEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNamesUnicode), ['Art', 'Artichoke', 'art', 'artichoke'], 'names with the same root and different cases sort uppercase first'); + assert.deepStrictEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileNamesUnicode), ['Email', 'email', 'Émail', 'émail'], 'the same base characters with different case or accents sort in unicode order'); + + // name plus extension comparisons + assert(compareFileNamesUnicode('aggregate.go', 'aggregate_repo.go') < 0, 'compares the whole name in unicode order, but dot comes before underscore'); + + // dotfile comparisons + assert(compareFileNamesUnicode('.aaa_env', '.aaa.env') > 0, 'an underscore in a dotfile name will sort after a dot'); + + // numeric comparisons + assert(compareFileNamesUnicode('abc2.txt', 'abc10.txt') > 0, 'filenames with numbers should be in unicode order even when they are multiple digits long'); + assert(compareFileNamesUnicode('abc02.txt', 'abc010.txt') > 0, 'filenames with numbers that have leading zeros sort in unicode order'); + assert(compareFileNamesUnicode('abc1.10.txt', 'abc1.2.txt') < 0, 'numbers with dots between them are sorted in unicode order'); + assert(compareFileNamesUnicode('abc02.txt', 'abc002.txt') > 0, 'filenames with equivalent numbers and leading zeros sort in unicode order'); + assert(compareFileNamesUnicode('abc.txt1', 'abc.txt01') > 0, 'same name plus extensions with equal numbers sort in unicode order'); + assert(compareFileNamesUnicode('art01', 'Art01') > 0, 'a numerically equivalent name of a different case compares uppercase first'); + assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileNamesUnicode), ['A100.txt', 'A2.txt', 'a10.txt', 'a20.txt'], 'filenames with number and case differences sort in unicode order'); + + }); + + test('compareFileExtensionsUnicode', () => { + + // + // Comparisons with the same result as compareFileExtensionsDefault + // + + // name-only comparisons + assert(compareFileExtensionsUnicode(null, null) === 0, 'null should be equal'); + assert(compareFileExtensionsUnicode(null, 'abc') < 0, 'null should come before real files without extensions'); + assert(compareFileExtensionsUnicode('', '') === 0, 'empty should be equal'); + assert(compareFileExtensionsUnicode('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileExtensionsUnicode('z', 'A') > 0, 'z comes after A'); + + // name plus extension comparisons + assert(compareFileExtensionsUnicode('file.ext', 'file.ext') === 0, 'equal full filenames should be equal'); + assert(compareFileExtensionsUnicode('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileExtensionsUnicode('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); + assert(compareFileExtensionsUnicode('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first'); + assert(compareFileExtensionsUnicode('a.md', 'b.MD') < 0, 'when extensions are the same except for case, the files sort by name'); + assert(compareFileExtensionsUnicode('a.MD', 'a.md') < 0, 'case differences in extensions sort in unicode order'); + + // dotfile comparisons + assert(compareFileExtensionsUnicode('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileExtensionsUnicode('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case'); + assert(compareFileExtensionsUnicode('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileExtensionsUnicode('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + + // dotfile vs non-dotfile comparisons + assert(compareFileExtensionsUnicode(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileExtensionsUnicode('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileExtensionsUnicode('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); + assert(compareFileExtensionsUnicode('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileExtensionsUnicode('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + + // numeric comparisons + assert(compareFileExtensionsUnicode('1', '1') === 0, 'numerically equal full names should be equal'); + assert(compareFileExtensionsUnicode('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); + assert(compareFileExtensionsUnicode('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsUnicode('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal'); + assert(compareFileExtensionsUnicode('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsUnicode('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, full filenames should be compared'); + + // + // Comparisons with different results than compareFileExtensionsDefault + // + + // name-only comparisons + assert(compareFileExtensionsUnicode('Z', 'a') < 0, 'Z comes before a'); + assert(compareFileExtensionsUnicode('a', 'A') > 0, 'the same letter sorts uppercase first'); + assert(compareFileExtensionsUnicode('â', 'Â') > 0, 'the same accented letter sorts uppercase first'); + assert.deepStrictEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensionsUnicode), ['Art', 'Artichoke', 'art', 'artichoke'], 'names with the same root and different cases sort uppercase names first'); + assert.deepStrictEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileExtensionsUnicode), ['Email', 'email', 'Émail', 'émail'], 'the same base characters with different case or accents sort in unicode order'); + + // name plus extension comparisons + assert(compareFileExtensionsUnicode('a.MD', 'a.md') < 0, 'case differences in extensions sort by uppercase extension first'); + assert(compareFileExtensionsUnicode('a.md', 'A.md') > 0, 'case differences in names sort uppercase first'); + assert(compareFileExtensionsUnicode('art01', 'Art01') > 0, 'a numerically equivalent name of a different case sorts uppercase first'); + assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileExtensionsUnicode), ['A100.txt', 'A2.txt', 'a10.txt', 'a20.txt'], 'filenames with number and case differences sort in unicode order'); + assert(compareFileExtensionsUnicode('aggregate.go', 'aggregate_repo.go') < 0, 'when extensions are equal, compares full filenames in unicode order'); + + // numeric comparisons + assert(compareFileExtensionsUnicode('abc2.txt', 'abc10.txt') > 0, 'filenames with numbers should be in unicode order'); + assert(compareFileExtensionsUnicode('abc02.txt', 'abc010.txt') > 0, 'filenames with numbers that have leading zeros sort in unicode order'); + assert(compareFileExtensionsUnicode('abc1.10.txt', 'abc1.2.txt') < 0, 'numbers with dots between them sort in unicode order'); + assert(compareFileExtensionsUnicode('abc2.txt2', 'abc1.txt10') > 0, 'extensions with numbers should be in unicode order'); + assert(compareFileExtensionsUnicode('txt.abc2', 'txt.abc10') > 0, 'extensions with numbers should be in unicode order even when they are multiple digits long'); + assert(compareFileExtensionsUnicode('abc.txt01', 'abc.txt1') < 0, 'extensions with equal numbers should be in unicode order'); + assert(compareFileExtensionsUnicode('abc02.txt', 'abc002.txt') > 0, 'filenames with equivalent numbers and leading zeros sort in unicode order'); + assert(compareFileExtensionsUnicode('txt.abc01', 'txt.abc1') < 0, 'extensions with equivalent numbers sort in unicode order'); + assert(compareFileExtensionsUnicode('a.ext1', 'b.Ext1') < 0, 'if extensions with numbers are equal except for case, unicode full filenames should be compared'); + assert(compareFileExtensionsUnicode('a.ext1', 'a.Ext1') > 0, 'if extensions with numbers are equal except for case, unicode full filenames should be compared'); + + }); + }); diff --git a/src/vs/base/test/common/arrays.test.ts b/src/vs/base/test/common/arrays.test.ts index 4f3c06cdb7..02d5654032 100644 --- a/src/vs/base/test/common/arrays.test.ts +++ b/src/vs/base/test/common/arrays.test.ts @@ -296,45 +296,19 @@ suite('Arrays', () => { assert.strictEqual(array.length, 0); }); - test('splice', function () { - // negative start index, absolute value greater than the length - let array = [1, 2, 3, 4, 5]; - arrays.splice(array, -6, 3, [6, 7]); - assert.strictEqual(array.length, 4); - assert.strictEqual(array[0], 6); - assert.strictEqual(array[1], 7); - assert.strictEqual(array[2], 4); - assert.strictEqual(array[3], 5); + test('minIndex', () => { + const array = ['a', 'b', 'c']; + assert.strictEqual(arrays.minIndex(array, value => array.indexOf(value)), 0); + assert.strictEqual(arrays.minIndex(array, value => -array.indexOf(value)), 2); + assert.strictEqual(arrays.minIndex(array, _value => 0), 0); + assert.strictEqual(arrays.minIndex(array, value => value === 'b' ? 0 : 5), 1); + }); - // negative start index, absolute value less than the length - array = [1, 2, 3, 4, 5]; - arrays.splice(array, -3, 3, [6, 7]); - assert.strictEqual(array.length, 4); - assert.strictEqual(array[0], 1); - assert.strictEqual(array[1], 2); - assert.strictEqual(array[2], 6); - assert.strictEqual(array[3], 7); - - // Start index less than the length - array = [1, 2, 3, 4, 5]; - arrays.splice(array, 3, 3, [6, 7]); - assert.strictEqual(array.length, 5); - assert.strictEqual(array[0], 1); - assert.strictEqual(array[1], 2); - assert.strictEqual(array[2], 3); - assert.strictEqual(array[3], 6); - assert.strictEqual(array[4], 7); - - // Start index greater than the length - array = [1, 2, 3, 4, 5]; - arrays.splice(array, 6, 3, [6, 7]); - assert.strictEqual(array.length, 7); - assert.strictEqual(array[0], 1); - assert.strictEqual(array[1], 2); - assert.strictEqual(array[2], 3); - assert.strictEqual(array[3], 4); - assert.strictEqual(array[4], 5); - assert.strictEqual(array[5], 6); - assert.strictEqual(array[6], 7); + test('maxIndex', () => { + const array = ['a', 'b', 'c']; + assert.strictEqual(arrays.maxIndex(array, value => array.indexOf(value)), 2); + assert.strictEqual(arrays.maxIndex(array, value => -array.indexOf(value)), 0); + assert.strictEqual(arrays.maxIndex(array, _value => 0), 0); + assert.strictEqual(arrays.maxIndex(array, value => value === 'b' ? 5 : 0), 1); }); }); diff --git a/src/vs/base/test/common/async.test.ts b/src/vs/base/test/common/async.test.ts index b6f65e5797..6e82c18b18 100644 --- a/src/vs/base/test/common/async.test.ts +++ b/src/vs/base/test/common/async.test.ts @@ -561,7 +561,7 @@ suite('Async', () => { }); test('TaskSequentializer - pending basics', async function () { - const sequentializer: any = new async.TaskSequentializer(); + const sequentializer: any = new async.TaskSequentializer(); // {{SQL CARBON EDIT}} Add any type assert.ok(!sequentializer.hasPending()); assert.ok(!sequentializer.hasPending(2323)); diff --git a/src/vs/base/test/common/codicon.test.ts b/src/vs/base/test/common/codicon.test.ts index da0fcb4d20..192c855f2a 100644 --- a/src/vs/base/test/common/codicon.test.ts +++ b/src/vs/base/test/common/codicon.test.ts @@ -4,75 +4,25 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IMatch } from 'vs/base/common/filters'; -import { matchesFuzzyCodiconAware, parseCodicons, IParsedCodicons } from 'vs/base/common/codicon'; -import { stripCodicons } from 'vs/base/common/codicons'; - -export interface ICodiconFilter { - // Returns null if word doesn't match. - (query: string, target: IParsedCodicons): IMatch[] | null; -} - -function filterOk(filter: ICodiconFilter, word: string, target: IParsedCodicons, highlights?: { start: number; end: number; }[]) { - let r = filter(word, target); - assert(r); - if (highlights) { - assert.deepEqual(r, highlights); - } -} +import { getCodiconAriaLabel } from 'vs/base/common/codicons'; suite('Codicon', () => { - test('matchesFuzzzyCodiconAware', () => { - - // Camel Case - - filterOk(matchesFuzzyCodiconAware, 'ccr', parseCodicons('$(codicon)CamelCaseRocks$(codicon)'), [ - { start: 10, end: 11 }, - { start: 15, end: 16 }, - { start: 19, end: 20 } + test('Can get proper aria labels', () => { + // note, the spaces in the results are important + const testCases = new Map([ + ['', ''], + ['asdf', 'asdf'], + ['asdf$(squirrel)asdf', 'asdf squirrel asdf'], + ['asdf $(squirrel) asdf', 'asdf squirrel asdf'], + ['$(rocket)asdf', 'rocket asdf'], + ['$(rocket) asdf', 'rocket asdf'], + ['$(rocket)$(rocket)$(rocket)asdf', 'rocket rocket rocket asdf'], + ['$(rocket) asdf $(rocket)', 'rocket asdf rocket'], + ['$(rocket)asdf$(rocket)', 'rocket asdf rocket'], ]); - filterOk(matchesFuzzyCodiconAware, 'ccr', parseCodicons('$(codicon) CamelCaseRocks $(codicon)'), [ - { start: 11, end: 12 }, - { start: 16, end: 17 }, - { start: 20, end: 21 } - ]); - - filterOk(matchesFuzzyCodiconAware, 'iut', parseCodicons('$(codicon) Indent $(octico) Using $(octic) Tpaces'), [ - { start: 11, end: 12 }, - { start: 28, end: 29 }, - { start: 43, end: 44 }, - ]); - - // Prefix - - filterOk(matchesFuzzyCodiconAware, 'using', parseCodicons('$(codicon) Indent Using Spaces'), [ - { start: 18, end: 23 }, - ]); - - // Broken Codicon - - filterOk(matchesFuzzyCodiconAware, 'codicon', parseCodicons('This $(codicon Indent Using Spaces'), [ - { start: 7, end: 14 }, - ]); - - filterOk(matchesFuzzyCodiconAware, 'indent', parseCodicons('This $codicon Indent Using Spaces'), [ - { start: 14, end: 20 }, - ]); - - // Testing #59343 - filterOk(matchesFuzzyCodiconAware, 'unt', parseCodicons('$(primitive-dot) $(file-text) Untitled-1'), [ - { start: 30, end: 33 }, - ]); - }); -}); - -suite('Codicons', () => { - - test('stripCodicons', () => { - assert.equal(stripCodicons('Hello World'), 'Hello World'); - assert.equal(stripCodicons('$(Hello World'), '$(Hello World'); - assert.equal(stripCodicons('$(Hello) World'), ' World'); - assert.equal(stripCodicons('$(Hello) W$(oi)rld'), ' Wrld'); + for (const [input, expected] of testCases) { + assert.strictEqual(getCodiconAriaLabel(input), expected); + } }); }); diff --git a/src/vs/base/test/common/codicons.test.ts b/src/vs/base/test/common/codicons.test.ts index 02a30dbe50..192c855f2a 100644 --- a/src/vs/base/test/common/codicons.test.ts +++ b/src/vs/base/test/common/codicons.test.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; diff --git a/src/vs/base/test/common/collections.test.ts b/src/vs/base/test/common/collections.test.ts index fecaad18b2..b194909720 100644 --- a/src/vs/base/test/common/collections.test.ts +++ b/src/vs/base/test/common/collections.test.ts @@ -53,26 +53,4 @@ suite('Collections', () => { assert.strictEqual(grouped[group2].length, 1); assert.strictEqual(grouped[group2][0].value, value3); }); - - test('groupByNumber', () => { - - const group1 = 1, group2 = 2; - const value1 = 'a', value2 = 'b', value3 = 'c'; - let source = [ - { key: group1, value: value1 }, - { key: group1, value: value2 }, - { key: group2, value: value3 }, - ]; - - let grouped = collections.groupByNumber(source, x => x.key); - - // Group 1 - assert.strictEqual(grouped.get(group1)!.length, 2); - assert.strictEqual(grouped.get(group1)![0].value, value1); - assert.strictEqual(grouped.get(group1)![1].value, value2); - - // Group 2 - assert.strictEqual(grouped.get(group2)!.length, 1); - assert.strictEqual(grouped.get(group2)![0].value, value3); - }); }); diff --git a/src/vs/base/test/common/decorators.test.ts b/src/vs/base/test/common/decorators.test.ts index 6b2e3f2b35..ed843b761e 100644 --- a/src/vs/base/test/common/decorators.test.ts +++ b/src/vs/base/test/common/decorators.test.ts @@ -5,7 +5,7 @@ import * as sinon from 'sinon'; import * as assert from 'assert'; -import { memoize, createMemoizer, throttle } from 'vs/base/common/decorators'; +import { memoize, throttle } from 'vs/base/common/decorators'; suite('Decorators', () => { test('memoize should memoize methods', () => { @@ -131,28 +131,6 @@ suite('Decorators', () => { } }); - test('memoize clear', () => { - const memoizer = createMemoizer(); - let counter = 0; - class Foo { - @memoizer - get answer() { - return ++counter; - } - } - - const foo = new Foo(); - assert.strictEqual(foo.answer, 1); - assert.strictEqual(foo.answer, 1); - memoizer.clear(); - assert.strictEqual(foo.answer, 2); - assert.strictEqual(foo.answer, 2); - memoizer.clear(); - assert.strictEqual(foo.answer, 3); - assert.strictEqual(foo.answer, 3); - assert.strictEqual(foo.answer, 3); - }); - test('throttle', () => { const spy = sinon.spy(); const clock = sinon.useFakeTimers(); @@ -183,7 +161,7 @@ suite('Decorators', () => { clock.tick(200); assert.deepStrictEqual(spy.args, [[1], [5]]); - spy.reset(); + spy.resetHistory(); t.report(4); t.report(5); diff --git a/src/vs/base/test/common/event.test.ts b/src/vs/base/test/common/event.test.ts index b0574699ac..db686b3528 100644 --- a/src/vs/base/test/common/event.test.ts +++ b/src/vs/base/test/common/event.test.ts @@ -3,7 +3,7 @@ * 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, PauseableEmitter } from 'vs/base/common/event'; +import { Event, Emitter, EventBufferer, EventMultiplexer, PauseableEmitter, Relay } from 'vs/base/common/event'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { errorHandler, setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { AsyncEmitter, IWaitUntil, timeout } from 'vs/base/common/async'; @@ -894,4 +894,70 @@ suite('Event utils', () => { listener.dispose(); }); + test('dispose is reentrant', () => { + const emitter = new Emitter({ + onLastListenerRemove: () => { + emitter.dispose(); + } + }); + + const listener = emitter.event(() => undefined); + listener.dispose(); // should not crash + }); + + suite('Relay', () => { + test('should input work', () => { + const e1 = new Emitter(); + const e2 = new Emitter(); + const relay = new Relay(); + + const result: number[] = []; + const listener = (num: number) => result.push(num); + const subscription = relay.event(listener); + + e1.fire(1); + assert.deepStrictEqual(result, []); + + relay.input = e1.event; + e1.fire(2); + assert.deepStrictEqual(result, [2]); + + relay.input = e2.event; + e1.fire(3); + e2.fire(4); + assert.deepStrictEqual(result, [2, 4]); + + subscription.dispose(); + e1.fire(5); + e2.fire(6); + assert.deepStrictEqual(result, [2, 4]); + }); + + test('should Relay dispose work', () => { + const e1 = new Emitter(); + const e2 = new Emitter(); + const relay = new Relay(); + + const result: number[] = []; + const listener = (num: number) => result.push(num); + relay.event(listener); + + e1.fire(1); + assert.deepStrictEqual(result, []); + + relay.input = e1.event; + e1.fire(2); + assert.deepStrictEqual(result, [2]); + + relay.input = e2.event; + e1.fire(3); + e2.fire(4); + assert.deepStrictEqual(result, [2, 4]); + + relay.dispose(); + e1.fire(5); + e2.fire(6); + assert.deepStrictEqual(result, [2, 4]); + }); + }); }); diff --git a/src/vs/base/test/common/filters.perf.data.d.ts b/src/vs/base/test/common/filters.perf.data.d.ts index 887ac60164..0fae2f91e9 100644 --- a/src/vs/base/test/common/filters.perf.data.d.ts +++ b/src/vs/base/test/common/filters.perf.data.d.ts @@ -2,4 +2,4 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export const data: string[]; \ No newline at end of file +export const data: string[]; diff --git a/src/vs/base/test/common/filters.perf.data.js b/src/vs/base/test/common/filters.perf.data.js index 3e7b446569..ce647316a6 100644 --- a/src/vs/base/test/common/filters.perf.data.js +++ b/src/vs/base/test/common/filters.perf.data.js @@ -4,4 +4,4 @@ *--------------------------------------------------------------------------------------------*/ define(function() { return { data: ["AI_ClearCaptureImportanceBonus","AI_ClearImportance","AI_CreateObjective","AI_DebugAttackEncounterPositionScoringEnable","AI_DebugAttackEncounterPositionScoringIsEnabled","AI_DebugLuaEnable","AI_DebugLuaIsEnabled","AI_DebugRatingEnable","AI_DebugRatingIsEnabled","AI_DebugRenderAllTaskChildrenEnable","AI_DebugRenderAllTaskChildrenIsEnabled","AI_DebugSkirmishCaptureEnable","AI_DebugSkirmishCaptureIsEnabled","AI_DebugSkirmishCombatTargetEnable","AI_DebugSkirmishCombatTargetIsEnabled","AI_DebugSkirmishObjectiveEnable","AI_DebugSkirmishObjectiveIsEnabled","AI_DisableAllEconomyOverrides","AI_Enable","AI_EnableAll","AI_EnableEconomyOverride","AI_GetDifficulty","AI_GetPersonality","AI_GetPersonalityLuaFileName","AI_IsAIPlayer","AI_IsEnabled","AI_LockEntity","AI_LockSquad","AI_LockSquads","AI_RestoreDefaultPersonalitySettings","AI_SetCaptureImportanceBonus","AI_SetDifficulty","AI_SetImportance","AI_SetPersonality","AI_UnlockAll","AI_UnlockEntity","AI_UnlockSquad","AI_UnlockSquads","AI_UpdateStatics","AIAbilityObjective_AbilityGuidance_SetAbilityPBG","AIObjective_Cancel","AIObjective_CombatGuidance_EnableCombatGarrison","AIObjective_CombatGuidance_EnableRetaliateAttacks","AIObjective_CombatGuidance_SetRetaliateAttackTargetAreaRadius","AIObjective_DefenseGuidance_AddFacingPosition","AIObjective_DefenseGuidance_EnableIdleGarrison","AIObjective_DefenseGuidance_ResetFacingPositions","AIObjective_EngagementGuidance_EnableAggressiveEngagementMove","AIObjective_EngagementGuidance_SetAllowReturnToPreviousStages","AIObjective_EngagementGuidance_SetCoordinatedSetup","AIObjective_EngagementGuidance_SetMaxEngagementTime","AIObjective_EngagementGuidance_SetMaxIdleTime","AIObjective_FallbackGuidance_EnableRetreatOnPinned","AIObjective_FallbackGuidance_EnableRetreatOnSuppression","AIObjective_FallbackGuidance_SetEntitiesRemainingThreshold","AIObjective_FallbackGuidance_SetFallbackCapacityPercentage","AIObjective_FallbackGuidance_SetFallbackCombatRatingPercentage","AIObjective_FallbackGuidance_SetFallbackSquadHealthPercentage","AIObjective_FallbackGuidance_SetFallbackVehicleHealthPercentage","AIObjective_FallbackGuidance_SetGlobalFallbackPercentage","AIObjective_FallbackGuidance_SetGlobalFallbackRetreat","AIObjective_FallbackGuidance_SetRetreatCapacityPercentage","AIObjective_FallbackGuidance_SetRetreatCombatRatingPercentage","AIObjective_FallbackGuidance_SetRetreatHealthPercentage","AIObjective_FallbackGuidance_SetTargetPosition","AIObjective_IsValid","AIObjective_MoveGuidance_EnableAggressiveMove","AIObjective_MoveGuidance_ResetPathingLengthFactor","AIObjective_MoveGuidance_ResetSafePathingWeight","AIObjective_MoveGuidance_SetPathingLengthFactor","AIObjective_MoveGuidance_SetSafePathingWeight","AIObjective_MoveGuidance_SetSquadCoherenceRadius","AIObjective_Notify_ClearCallbacks","AIObjective_Notify_SetPlayerEventObjectiveID","AIObjective_ResourceGuidance_ClearSquads","AIObjective_ResourceGuidance_SquadGroup","AIObjective_SetName","AIObjective_TacticFilter_DisableAbility","AIObjective_TacticFilter_DisableAbilityForSquadGroup","AIObjective_TacticFilter_EnableCloseGround","AIObjective_TacticFilter_Reset","AIObjective_TacticFilter_ResetAbilityGuidance","AIObjective_TacticFilter_ResetPriority","AIObjective_TacticFilter_ResetTacticGuidance","AIObjective_TacticFilter_ResetTargetGuidance","AIObjective_TacticFilter_SetAbilityGuidance","AIObjective_TacticFilter_SetDefaultAbilityGuidance","AIObjective_TacticFilter_SetDefaultTacticGuidance","AIObjective_TacticFilter_SetDefaultTargetGuidance","AIObjective_TacticFilter_SetPriority","AIObjective_TacticFilter_SetPriorityForSquadGroup","AIObjective_TacticFilter_SetTacticGuidance","AIObjective_TacticFilter_SetTargetPolicy","AIObjective_TargetGuidance_SetTargetArea","AIObjective_TargetGuidance_SetTargetEntity","AIObjective_TargetGuidance_SetTargetLeash","AIObjective_TargetGuidance_SetTargetPathByName","AIObjective_TargetGuidance_SetTargetPathWander","AIObjective_TargetGuidance_SetTargetPosition","AIObjective_TargetGuidance_SetTargetSquad","BeginnerHint_AddOpportunity","BeginnerHint_RemoveAllOpportunities","BeginnerHint_RemoveOpportunity","BP_GetAbilityBlueprint","BP_GetCamouflageStanceBlueprint","BP_GetCriticalBlueprint","BP_GetEntityBlueprint","BP_GetID","BP_GetMoveTypeBlueprint","BP_GetName","BP_GetPropertyBagGroupCount","BP_GetPropertyBagGroupPathName","BP_GetSlotItemBlueprint","BP_GetSquadBlueprint","BP_GetUpgradeBlueprint","BP_GetWeaponBlueprint","EBP_Exists","SBP_Exists","Camera_CyclePositions","Camera_Follow","Camera_MoveTo","Camera_MoveToIfClose","Camera_SetDefault","Cmd_AbandonTeamWeapon","Cmd_Ability","Cmd_AttachSquads","Cmd_Attack","Cmd_AttackMove","Cmd_AttackMoveThenCapture","Cmd_CaptureTeamWeapon","Cmd_Construct","Cmd_CriticalHit","Cmd_DetonateDemolitions","Cmd_EjectOccupants","Cmd_Garrison","Cmd_InstantReinforceUnit","Cmd_InstantReinforceUnitPos","Cmd_InstantSetupTeamWeapon","Cmd_InstantUpgrade","Cmd_Move","Cmd_MoveAwayFromPos","Cmd_MoveToAndDespawn","Cmd_MoveToClosestMarker","Cmd_MoveToThenCapture","Cmd_RecrewVehicle","Cmd_ReinforceUnit","Cmd_ReinforceUnitPos","Cmd_Retreat","Cmd_RevertOccupiedBuilding","Cmd_SetDemolitions","Cmd_SquadCamouflageStance","Cmd_SquadPath","Cmd_SquadPatrolMarker","Cmd_StaggeredRetreat","Cmd_Stop","Cmd_Surrender","Cmd_UngarrisonSquad","Cmd_Upgrade","Command_Entity","Command_EntityAbility","Command_EntityBuildSquad","Command_EntityEntity","Command_EntityExt","Command_EntityPos","Command_EntityPosAbility","Command_EntityPosDirAbility","Command_EntityPosSquad","Command_EntitySquad","Command_EntityTargetEntityAbility","Command_EntityTargetSquadAbility","Command_EntityUpgrade","Command_Player","Command_PlayerAbility","Command_PlayerEntity","Command_PlayerEntityCriticalHit","Command_PlayerExt","Command_PlayerPos","Command_PlayerPosAbility","Command_PlayerPosDirAbility","Command_PlayerPosExt","Command_PlayerSquadConstructBuilding","Command_PlayerSquadConstructFence","Command_PlayerSquadConstructField","Command_PlayerSquadCriticalHit","Command_PlayerUpgrade","Command_Squad","Command_SquadAbility","Command_SquadAttackMovePos","Command_SquadDoCustomPlan","Command_SquadDoCustomPlanTarget","Command_SquadEntity","Command_SquadEntityAbility","Command_SquadEntityAttack","Command_SquadEntityBool","Command_SquadEntityExt","Command_SquadEntityLoad","Command_SquadExt","Command_SquadMovePos","Command_SquadMovePosFacing","Command_SquadPos","Command_SquadPosAbility","Command_SquadPosExt","Command_SquadPositionAttack","Command_SquadSquad","Command_SquadSquadAbility","Command_SquadSquadAttack","Command_SquadSquadExt","Command_SquadSquadLoad","Command_SquadUpgrade","AutoCinematic","AutoReinforce_AddSGroup","AutoReinforce_RemoveAll","AutoReinforce_RemoveSGroup","AutoRetreat_AddSGroup","AutoRetreat_RemoveAll","AutoRetreat_RemoveSGroup","BridgeTerritory_Add","Ceasefire_AddSGroup","Ceasefire_RemoveSGroup","FireTargettingArtillery","Game_DefaultGameRestore","Game_GetGameRestoreCallbackExists","Game_RemoveGameRestoreCallback","Game_SetGameRestoreCallback","Resources_Disable","Resources_Enable","ShootTheSky_AddSyncWeapon","ShootTheSky_RemoveAll","ShootTheSky_RemoveSyncWeapon","SmokeEntrance_Do","Table_Contains","Table_Copy","Table_GetRandomItem","TeamWeapon_AddGroup","TeamWeapon_RemoveDirections","TeamWeapon_RemoveGroup","EGroup_Add","EGroup_AddEGroup","EGroup_CanSeeEGroup","EGroup_CanSeeSGroup","EGroup_Clear","EGroup_Compare","EGroup_ContainsBlueprints","EGroup_ContainsEGroup","EGroup_ContainsEntity","EGroup_Count","EGroup_CountAlive","EGroup_CountDeSpawned","EGroup_CountSpawned","EGroup_Create","EGroup_CreateIfNotFound","EGroup_CreateKickerMessage","EGroup_DeSpawn","EGroup_Destroy","EGroup_DestroyAllEntities","EGroup_Duplicate","EGroup_EnableMinimapIndicator","EGroup_EnableUIDecorator","EGroup_Exists","EGroup_Filter","EGroup_FilterUnderConstruction","EGroup_ForEach","EGroup_ForEachAllOrAny","EGroup_ForEachAllOrAnyEx","EGroup_ForEachEx","EGroup_FromName","EGroup_GetAvgHealth","EGroup_GetDeSpawnedEntityAt","EGroup_GetInvulnerable","EGroup_GetLastAttacker","EGroup_GetName","EGroup_GetOffsetPosition","EGroup_GetPosition","EGroup_GetRandomSpawnedEntity","EGroup_GetSequence","EGroup_GetSpawnedEntityAt","EGroup_GetSpawnedEntityFilter","EGroup_GetSpread","EGroup_GetSquadsHeld","EGroup_HasUpgrade","EGroup_Hide","EGroup_InstantCaptureStrategicPoint","EGroup_InstantRevertOccupiedBuilding","EGroup_Intersection","EGroup_IsBurning","EGroup_IsCapturedByPlayer","EGroup_IsCapturedByTeam","EGroup_IsDoingAttack","EGroup_IsEmpty","EGroup_IsHoldingAny","EGroup_IsInCover","EGroup_IsMoving","EGroup_IsOnScreen","EGroup_IsProducingSquads","EGroup_IsSpawned","EGroup_IsUnderAttack","EGroup_IsUnderAttackByPlayer","EGroup_IsUnderAttackFromDirection","EGroup_IsUsingAbility","EGroup_Kill","EGroup_NotifyOnPlayerDemolition","EGroup_Remove","EGroup_RemoveDemolitions","EGroup_RemoveGroup","EGroup_RemoveUpgrade","EGroup_ReSpawn","EGroup_SetAnimatorAction","EGroup_SetAnimatorEvent","EGroup_SetAnimatorState","EGroup_SetAnimatorVariable","EGroup_SetAutoTargetting","EGroup_SetAvgHealth","EGroup_SetCrushable","EGroup_SetDemolitions","EGroup_SetHealthMinCap","EGroup_SetInvulnerable","EGroup_SetPlayerOwner","EGroup_SetRallyPoint","EGroup_SetRecrewable","EGroup_SetSelectable","EGroup_SetSharedProductionQueue","EGroup_SetStrategicPointNeutral","EGroup_SetWorldOwned","EGroup_Single","SGroup_HasEntityUpgrade","Ai\\:GetEncountersBySGroup","Ai\\:GetEncountersBySquad","AI_DisableAllEncounters","AI_EnableAllEncounters","AI_GetActiveEncounters","AI_GetNumEncounters","AI_IsMatchingDifficulty","AI_OverrideDifficulty","AI_RemoveAllEncounters","AI_SetDebugLevel","AI_SetStaggeredSpawnDelay","AI_ToggleDebugData","AI_ToggleDebugPrint","AIAbilityGoal_AdjustDefaultGoalData","AIAbilityGoal_SetDefaultGoalData","AIAbilityGoal_SetModifyGoalData","AIAbilityGoal_SetOverrideGoalData","AIAttackGoal_AdjustDefaultGoalData","AIAttackGoal_SetDefaultGoalData","AIAttackGoal_SetModifyGoalData","AIAttackGoal_SetOverrideGoalData","AIBaseGoal_AdjustDefaultGoalData","AIBaseGoal_SetDefaultGoalData","AIBaseGoal_SetModifyGoalData","AIBaseGoal_SetOverrideGoalData","AIDefendGoal_AdjustDefaultGoalData","AIDefendGoal_SetDefaultGoalData","AIDefendGoal_SetModifyGoalData","AIDefendGoal_SetOverrideGoalData","AIMoveGoal_AdjustDefaultGoalData","AIMoveGoal_SetDefaultGoalData","AIMoveGoal_SetModifyGoalData","AIMoveGoal_SetOverrideGoalData","Encounter\\:AddSgroup","Encounter\\:ClearGoal","Encounter\\:ConvertSgroup","Encounter\\:Create","Encounter\\:CreateAbility","Encounter\\:CreateAttack","Encounter\\:CreateBasic","Encounter\\:CreateDefend","Encounter\\:CreateMove","Encounter\\:CreatePatrol","Encounter\\:Disable","Encounter\\:Enable","Encounter\\:GetGoalData","Encounter\\:GetSgroup","Encounter\\:RemoveOnDeath","Encounter\\:RestartGoal","Encounter\\:SetGoal","Encounter\\:SetGoalOnSuccess","Encounter\\:SetOnDeath","Encounter\\:Spawn","Encounter\\:UpdateGoal","MergeClone","Entity_ApplyCritical","Entity_BuildingPanelInfo","Entity_CanAttackNow","Entity_CancelProductionQueueItem","Entity_CanLoadSquad","Entity_CanLoadSquadAndAttackCurrentTarget","Entity_CanSeeEntity","Entity_CanSeeSquad","Entity_ClearPostureSuggestion","Entity_ClearTagDebug","Entity_CompleteUpgrade","Entity_Create","Entity_CreateENV","Entity_DeSpawn","Entity_Destroy","Entity_DisableBuildingDeath","Entity_DoBuildingDamageRay","Entity_EnableAttention","Entity_EnableProductionQueue","Entity_EnableStrategicPoint","Entity_ForceConstruct","Entity_FromWorldID","Entity_GetActiveCommand","Entity_GetBlueprint","Entity_GetBuildingProgress","Entity_GetCoverValue","Entity_GetGameID","Entity_GetHeading","Entity_GetHealth","Entity_GetHealthMax","Entity_GetHealthPercentage","Entity_GetInvulnerable","Entity_GetInvulnerableMinCap","Entity_GetInvulnerableToCritical","Entity_GetLastAttacker","Entity_GetLastAttackers","Entity_GetMaxCaptureCrewSize","Entity_GetOffsetPosition","Entity_GetPlayerOwner","Entity_GetPosition","Entity_GetProductionQueueItem","Entity_GetProductionQueueItemType","Entity_GetProductionQueueSize","Entity_GetResourceType","Entity_GetSightInnerHeight","Entity_GetSightInnerRadius","Entity_GetSightOuterHeight","Entity_GetSightOuterRadius","Entity_GetSquad","Entity_GetSquadsHeld","Entity_GetTotalPanelCount","Entity_GetUndestroyedPanelCount","Entity_GetWeaponBlueprint","Entity_GetWeaponHardpointCount","Entity_HasAnyCritical","Entity_HasCritical","Entity_HasProductionQueue","Entity_HasUpgrade","Entity_InstantCaptureStrategicPoint","Entity_InstantRevertOccupiedBuilding","Entity_IsAlive","Entity_IsAttacking","Entity_IsBuilding","Entity_IsBurning","Entity_IsCamouflaged","Entity_IsCapturableBuilding","Entity_IsCasualty","Entity_IsCuttable","Entity_IsDemolitionReady","Entity_IsEBPBuilding","Entity_IsEBPObjCover","Entity_IsHardpointActive","Entity_IsHoldingAny","Entity_IsInCover","Entity_IsMoving","Entity_IsOfType","Entity_IsPartOfSquad","Entity_IsPlane","Entity_IsSlotItem","Entity_IsSoldier","Entity_IsSpawned","Entity_IsStartingPosition","Entity_IsStrategicPoint","Entity_IsStrategicPointCapturedBy","Entity_IsSyncWeapon","Entity_IsUnderAttack","Entity_IsUnderAttackByPlayer","Entity_IsUnderAttackFromDirection","Entity_IsValid","Entity_IsVaultable","Entity_IsVehicle","Entity_IsVictoryPoint","Entity_Kill","Entity_NotifyOnPlayerDemolition","Entity_RemoveBoobyTraps","Entity_RemoveCritical","Entity_RemoveDemolitions","Entity_RemoveUpgrade","Entity_SetAnimatorAction","Entity_SetAnimatorActionParameter","Entity_SetAnimatorEvent","Entity_SetAnimatorState","Entity_SetAnimatorVariable","Entity_SetBuildingVisualFireState","Entity_SetCrushable","Entity_SetCrushMode","Entity_SetDemolitions","Entity_SetEnableCasualty","Entity_SetHeading","Entity_SetHealth","Entity_SetInvulnerable","Entity_SetInvulnerableMinCap","Entity_SetInvulnerableToCritical","Entity_SetOnFire","Entity_SetPlayerOwner","Entity_SetPosition","Entity_SetProjectileCanExplode","Entity_SetRecrewable","Entity_SetSharedProductionQueue","Entity_SetStrategicPointNeutral","Entity_SetWorldOwned","Entity_SimHide","Entity_Spawn","Entity_StopAbility","Entity_SuggestPosture","Entity_SupportsDemolition","Entity_TagDebug","Entity_VisHide","Misc_DoWeaponHitEffectOnPosition","Misc_GetTerrainHeight","Misc_ToggleEntities","ModMisc_MakeCasualtyAction","ModMisc_MakeWreckAction","ModMisc_OOCAction","UI_EnableEntityDecorator","UI_EnableEntityMinimapIndicator","UI_EnableEntitySelectionVisuals","UI_EnableSquadDecorator","UI_EnableSquadMinimapIndicator","UI_GetAbilityIconName","Event_CreateAND","Event_CreateOR","Event_ElementOnScreen","Event_EncounterIsDead","Event_Exists","Event_GroupBurning","Event_GroupIsDead","Event_GroupIsNotPinned","Event_GroupIsNotSuppressed","Event_GroupIsPinned","Event_GroupIsSuppressed","Event_GroupLeftAlive","Event_IsDoingAttack","Event_IsEngaged","Event_IsHoldingAny","Event_IsInHold","Event_IsSelected","Event_IsUnderAttack","Event_NarrativeEventsNotRunning","Event_NarrativeEventsRunning","Event_OnHealth","Event_PlayerBuildingCount","Event_PlayerCanNotSeeElement","Event_PlayerCanSeeElement","Event_PlayerDoesntOwnTerritory","Event_PlayerOwnsElement","Event_PlayerOwnsTerritory","Event_PlayerResourceLevel","Event_PlayerSquadCount","Event_Proximity","Event_Remove","Event_RemoveAll","Event_TeamBuildingCount","Event_TeamCanNotSeeElement","Event_TeamCanSeeElement","Event_TeamDoesntOwnTerritory","Event_TeamOwnsElement","Event_TeamOwnsTerritory","Event_TeamResourceLevel","Event_TeamSquadCount","Event_Timer","Event_ToggleDebug","Event_View","EventHandler_AssignEncounterGoal","EventHandler_ObjectiveComplete","EventHandler_ObjectiveStart","EventHandler_RemoveHint","EventHandler_RemoveMinimapBlip","EventHandler_RemoveObjectiveUI","EventHandler_Retreat","EventHandler_StaggeredRetreat","EventHandler_StartIntel","EventHandler_StartNislet","EventHandler_StopFlashing","FOW_PlayerExploreAll","FOW_PlayerRevealAll","FOW_PlayerRevealArea","FOW_PlayerUnExploreAll","FOW_PlayerUnRevealAll","FOW_PlayerUnRevealArea","FOW_RevealAll","FOW_RevealArea","FOW_RevealEGroup","FOW_RevealEGroupOnly","FOW_RevealEntity","FOW_RevealMarker","FOW_RevealSGroup","FOW_RevealSGroupOnly","FOW_RevealSquad","FOW_RevealTerritory","FOW_UnRevealAll","FOW_UnRevealArea","FOW_UnRevealMarker","FOW_UnRevealTerritory","EGroup_CreateTable","EGroup_GetWBTable","Marker_GetNonSequentialTable","Marker_GetTable","SGroup_CreateTable","SGroup_GetWBTable","Marker_DoesNumberAttributeExist","Marker_DoesStringAttributeExist","Marker_Exists","Marker_FromName","Marker_GetDirection","Marker_GetName","Marker_GetNumberAttribute","Marker_GetPosition","Marker_GetProximityRadius","Marker_GetProximityType","Marker_GetSequence","Marker_GetStringAttribute","Marker_GetType","Marker_InProximity","Modifier_IsEnabledOnEGroup","Modifier_Remove","Modifier_RemoveAllFromEGroup","Modifier_RemoveAllFromSGroup","Modify_AbilityDelayTime","Modify_AbilityDurationTime","Modify_AbilityManpowerCost","Modify_AbilityMaxCastRange","Modify_AbilityMinCastRange","Modify_AbilityMunitionsCost","Modify_AbilityRechargeTime","Modify_Armor","Modify_CaptureTime","Modify_DisableHold","Modify_Enable_ParadropReinforcements","Modify_EntityBuildTime","Modify_EntityCost","Modify_PlayerExperienceReceived","Modify_PlayerProductionRate","Modify_PlayerResourceCap","Modify_PlayerResourceGift","Modify_PlayerResourceRate","Modify_PlayerSightRadius","Modify_ProductionRate","Modify_ProjectileDelayTime","Modify_ReceivedAccuracy","Modify_ReceivedDamage","Modify_ReceivedSuppression","Modify_SetUpgradeCost","Modify_SightRadius","Modify_SquadAvailability","Modify_SquadCaptureRate","Modify_SquadTypeSightRadius","Modify_TargetPriority","Modify_TeamWeapon","Modify_TerritoryRadius","Modify_UnitSpeed","Modify_UnitVeterancyValue","Modify_UpgradeBuildTime","Modify_Upkeep","Modify_VehicleRepairRate","Modify_VehicleRotationSpeed","Modify_VehicleTurretRotationSpeed","Modify_Vulnerability","Modify_WeaponAccuracy","Modify_WeaponBurstLength","Modify_WeaponBurstRateOfFire","Modify_WeaponCooldown","Modify_WeaponDamage","Modify_WeaponEnabled","Modify_WeaponPenetration","Modify_WeaponRange","Modify_WeaponReload","Modify_WeaponScatter","Modify_WeaponSuppression","MP_BlizzardInit","Objective_AddPing","Objective_AddUIElements","Objective_AreAllPrimaryObjectivesComplete","Objective_Complete","Objective_Fail","Objective_GetCounter","Objective_GetTimerSeconds","Objective_IncreaseCounter","Objective_IsComplete","Objective_IsCounterSet","Objective_IsFailed","Objective_IsStarted","Objective_IsTimerSet","Objective_IsVisible","Objective_PauseTimer","Objective_Register","Objective_RemovePing","Objective_RemoveUIElements","Objective_ResumeTimer","Objective_SetAlwaysShowDetails","Objective_SetCounter","Objective_Show","Objective_Start","Objective_StartTimer","Objective_StopCounter","Objective_StopTimer","Objective_TogglePings","Objective_UpdateText","Cmd_StopSquadsOnly","OpGameSetup","OpNPC_AddSupportGroup","OpNPC_AddSyncWpnGroup","OpNPC_AddTeamWpnGroup","OpNPC_IsGroupActive","OpNPC_Name","OpNPC_RemoveGroup","OpNPC_RetreatGroup","OpNPC_SetGroupActive","OpPlayer_Action","OpUtil_AddModifier","OpUtil_AddResourcesToTeam","OpUtil_AssignSquadSameTypeControlGroup","OpUtil_AssignSquadUnusedControlGroup","OpUtil_ClearPlayZone","OpUtil_EgroupIsCapturedByTeam","OpUtil_EnemyEGroupArrowManager","OpUtil_FindNearestCapturePoint","OpUtil_InvulnerableAdd","OpUtil_InvulnerableRemove","OpUtil_LogSyncWpn","OpUtil_ReturnEnemyNPC","OpUtil_ReturnHumanPlayer","OpUtil_ReturnNPCPlayer","OpUtil_ReturnRace","OpUtil_ReturnTeam","OpUtil_SetPlayZone","OpUtil_TeamOwnsEntity","OpVP_AddPenaltyGroup","OpVP_Name","OpVP_RegisterCaptureablePoints","OpVP_RegisterPointDefense","OpVP_RemoveGroup","UI_PopUpMessage","Util_ProductionRestriction","Util_TutorialIntel","Player_AddAbility","Player_AddAbilityLockoutZone","Player_AddResource","Player_AddSquadsToSGroup","Player_AddUnspentCommandPoints","Player_AreSquadsNearMarker","Player_CanCastAbilityOnEntity","Player_CanCastAbilityOnPlayer","Player_CanCastAbilityOnPosition","Player_CanCastAbilityOnSquad","Player_CanSeeEGroup","Player_CanSeeEntity","Player_CanSeePosition","Player_CanSeeSGroup","Player_CanSeeSquad","Player_ClearArea","Player_ClearAvailabilities","Player_ClearPopCapOverride","Player_CompleteUpgrade","Player_DoParadrop","Player_FindFirstEnemyPlayer","Player_FromId","Player_GetAIType","Player_GetAll","Player_GetAllEntitiesNearMarker","Player_GetAllSquadsNearMarker","Player_GetBuildingID","Player_GetBuildingsCount","Player_GetBuildingsCountExcept","Player_GetBuildingsCountOnly","Player_GetCurrentPopulation","Player_GetDisplayName","Player_GetEntities","Player_GetEntitiesFromType","Player_GetEntityConcentration","Player_GetEntityCount","Player_GetEntityName","Player_GetID","Player_GetMaxPopulation","Player_GetNumStrategicPoints","Player_GetNumVictoryPoints","Player_GetPopulationPercentage","Player_GetRace","Player_GetRaceName","Player_GetRelationship","Player_GetResource","Player_GetResourceRate","Player_GetSquadConcentration","Player_GetSquadCount","Player_GetSquads","Player_GetStartingPosition","Player_GetStrategicPointCaptureProgress","Player_GetTeam","Player_GetUnitCount","Player_GetUpgradeCost","Player_HasAbility","Player_HasBuilding","Player_HasBuildingsExcept","Player_HasBuildingUnderConstruction","Player_HasCapturingSquadNearStrategicPoint","Player_HasLost","Player_HasMapEntryPosition","Player_HasUpgrade","Player_IsAlive","Player_IsAllied","Player_IsHuman","Player_NumUpgradeComplete","Player_OwnsEGroup","Player_OwnsEntity","Player_OwnsSGroup","Player_OwnsSquad","Player_RemoveAbilityLockoutZone","Player_RemoveUpgrade","Player_ResetResource","Player_RestrictAddOnList","Player_RestrictBuildingList","Player_RestrictResearchList","Player_SetAbilityAvailability","Player_SetAllCommandAvailabilityInternal","Player_SetCommandAvailability","Player_SetConstructionMenuAvailability","Player_SetDefaultSquadMoodMode","Player_SetEntityProductionAvailability","Player_SetHeatGainRate","Player_SetHeatLossRate","Player_SetMaxCapPopulation","Player_SetMaxPopulation","Player_SetPopCapOverride","Player_SetResource","Player_SetSquadProductionAvailability","Player_SetUpgradeAvailability","Player_SetUpgradeCost","Player_SpawnGlider","Player_StopAbility","Player_StopEarningActionPoints","Player_Triangulate","Actor_Clear","Actor_PlaySpeech","Actor_PlaySpeechWithoutPortrait","Actor_SetFromSGroup","Actor_SetFromSquad","Prox_AreEntitiesNearMarker","Prox_ArePlayerMembersNearMarker","Prox_ArePlayersNearMarker","Prox_AreSquadMembersNearMarker","Prox_AreSquadsNearMarker","Prox_AreTeamsNearMarker","Prox_EGroupEGroup","Prox_EGroupSGroup","Prox_EntitiesInProximityOfEntities","Prox_GetRandomPosition","Prox_MarkerEGroup","Prox_MarkerSGroup","Prox_PlayerEntitiesInProximityOfEntities","Prox_PlayerEntitiesInProximityOfPlayerSquads","Prox_PlayerEntitiesInProximityOfSquads","Prox_PlayerSquadsInProximityOfEntities","Prox_PlayerSquadsInProximityOfPlayerEntities","Prox_PlayerSquadsInProximityOfPlayerSquads","Prox_PlayerSquadsInProximityOfSquads","Prox_SGroupSGroup","Prox_SquadsInProximityOfEntities","Prox_SquadsInProximityOfSquads","Rule_Add","Rule_AddDelayedInterval","Rule_AddDelayedIntervalEx","Rule_AddEGroupEvent","Rule_AddEntityEvent","Rule_AddGlobalEvent","Rule_AddInterval","Rule_AddIntervalEx","Rule_AddOneShot","Rule_AddPlayerEvent","Rule_AddSGroupEvent","Rule_AddSquadEvent","Rule_ChangeInterval","Rule_Exists","Rule_Remove","Rule_RemoveAll","Rule_RemoveEGroupEvent","Rule_RemoveEntityEvent","Rule_RemoveGlobalEvent","Rule_RemoveIfExist","Rule_RemoveMe","Rule_RemovePlayerEvent","Rule_RemoveSGroupEvent","Rule_RemoveSquadEvent","Setup_Player","Cmd_StopSquadsExcept","Misc_IsEGroupOnScreen","Misc_IsSGroupOnScreen","SGroup_Add","SGroup_AddAbility","SGroup_AddGroup","SGroup_AddGroups","SGroup_AddLeaders","SGroup_AddSlotItemToDropOnDeath","SGroup_CanCastAbilityOnEntity","SGroup_CanCastAbilityOnPosition","SGroup_CanCastAbilityOnSquad","SGroup_CanInstantReinforceNow","SGroup_CanSeeSGroup","SGroup_Clear","SGroup_ClearPostureSuggestion","SGroup_Compare","SGroup_CompleteEntityUpgrade","SGroup_ContainsBlueprints","SGroup_ContainsSGroup","SGroup_ContainsSquad","SGroup_Count","SGroup_CountDeSpawned","SGroup_CountSpawned","SGroup_Create","SGroup_CreateIfNotFound","SGroup_CreateKickerMessage","SGroup_DeSpawn","SGroup_Destroy","SGroup_DestroyAllInMarker","SGroup_DestroyAllSquads","SGroup_DisableCombatPlans","SGroup_Duplicate","SGroup_EnableAttention","SGroup_EnableMinimapIndicator","SGroup_EnableSurprise","SGroup_EnableUIDecorator","SGroup_Exists","SGroup_FaceEachOther","SGroup_FaceMarker","SGroup_Filter","SGroup_FilterCount","SGroup_FilterThreat","SGroup_ForEach","SGroup_ForEachAllOrAny","SGroup_ForEachAllOrAnyEx","SGroup_ForEachEx","SGroup_FromName","SGroup_GetAvgHealth","SGroup_GetAvgLoadout","SGroup_GetDeSpawnedSquadAt","SGroup_GetGarrisonedBuildingEntity","SGroup_GetHoldEGroup","SGroup_GetHoldSGroup","SGroup_GetInvulnerable","SGroup_GetLastAttacker","SGroup_GetLoadedVehicleSquad","SGroup_GetName","SGroup_GetNumSlotItem","SGroup_GetOffsetPosition","SGroup_GetPosition","SGroup_GetRandomSpawnedSquad","SGroup_GetSequence","SGroup_GetSpawnedSquadAt","SGroup_GetSpread","SGroup_GetSquadsHeld","SGroup_GetSuppression","SGroup_GetVeterancyExperience","SGroup_GetVeterancyRank","SGroup_HasCritical","SGroup_HasLeader","SGroup_HasSquadBlueprint","SGroup_HasTeamWeapon","SGroup_HasUpgrade","SGroup_Hide","SGroup_IncreaseVeterancyExperience","SGroup_IncreaseVeterancyRank","SGroup_Intersection","SGroup_IsAlive","SGroup_IsAttackMoving","SGroup_IsCamouflaged","SGroup_IsCapturing","SGroup_IsConstructingBuilding","SGroup_IsDoingAbility","SGroup_IsDoingAttack","SGroup_IsDugIn","SGroup_IsEmpty","SGroup_IsFemale","SGroup_IsHoldingAny","SGroup_IsIdle","SGroup_IsInCover","SGroup_IsInfiltrated","SGroup_IsInHoldEntity","SGroup_IsInHoldSquad","SGroup_IsMoving","SGroup_IsOnScreen","SGroup_IsPinned","SGroup_IsReinforcing","SGroup_IsRetreating","SGroup_IsSettingDemolitions","SGroup_IsSuppressed","SGroup_IsUnderAttack","SGroup_IsUnderAttackByPlayer","SGroup_IsUnderAttackFromDirection","SGroup_IsUpgrading","SGroup_IsUsingAbility","SGroup_Kill","SGroup_Remove","SGroup_RemoveGroup","SGroup_RemoveUpgrade","SGroup_ReSpawn","SGroup_RestoreCombatPlans","SGroup_RewardActionPoints","SGroup_SetAnimatorState","SGroup_SetAutoTargetting","SGroup_SetAvgHealth","SGroup_SetAvgMorale","SGroup_SetCrushable","SGroup_SetInvulnerable","SGroup_SetInvulnerableToCritical","SGroup_SetMoodMode","SGroup_SetMoveType","SGroup_SetPlayerOwner","SGroup_SetRecrewable","SGroup_SetSelectable","SGroup_SetSharedProductionQueue","SGroup_SetSuppression","SGroup_SetTeamWeaponCapturable","SGroup_SetVeterancyDisplayVisibility","SGroup_SetWorldOwned","SGroup_Single","SGroup_SnapFaceEachOther","SGroup_SuggestPosture","SGroup_TotalMembersCount","SGroup_WarpToMarker","SGroup_WarpToPos","Util_Grab","SGroup_FacePosition","SGroup_SnapFacePosition","Squad_AddAbility","Squad_AddSlotItemToDropOnDeath","Squad_CanCaptureStrategicPoint","Squad_CanCaptureTeamWeapon","Squad_CanCastAbilityOnEGroup","Squad_CanCastAbilityOnEntity","Squad_CanCastAbilityOnPosition","Squad_CanCastAbilityOnSGroup","Squad_CanCastAbilityOnSquad","Squad_CancelProductionQueueItem","Squad_CanHold","Squad_CanInstantReinforceNow","Squad_CanLoadSquad","Squad_CanPickupSlotItem","Squad_CanRecrew","Squad_CanSeeEntity","Squad_CanSeeSquad","Squad_ClearPostureSuggestion","Squad_CompleteUpgrade","Squad_Count","Squad_CreateAndSpawnToward","Squad_DeSpawn","Squad_Destroy","Squad_EnableProductionQueue","Squad_EnableSurprise","Squad_EntityAt","Squad_FacePosition","Squad_FaceSquad","Squad_FindCover","Squad_FindCoverCompareCurrent","Squad_FromWorldID","Squad_GetActiveCommand","Squad_GetAttackPlan","Squad_GetAttackTargets","Squad_GetBlueprint","Squad_GetDestination","Squad_GetGameID","Squad_GetHeading","Squad_GetHealth","Squad_GetHealthMax","Squad_GetHealthPercentage","Squad_GetHoldEntity","Squad_GetHoldSquad","Squad_GetInvulnerable","Squad_GetInvulnerableEntityCount","Squad_GetInvulnerableMinCap","Squad_GetLastAttacker","Squad_GetLastAttackers","Squad_GetLastEntityAttacker","Squad_GetMax","Squad_GetNumSlotItem","Squad_GetOffsetPosition","Squad_GetPinnedPlan","Squad_GetPlayerOwner","Squad_GetPosition","Squad_GetPositionDeSpawned","Squad_GetProductionQueueItem","Squad_GetProductionQueueItemType","Squad_GetProductionQueueSize","Squad_GetReactionPlan","Squad_GetRetaliationPlan","Squad_GetSlotItemAt","Squad_GetSlotItemCount","Squad_GetSlotItemsTable","Squad_GetSquadsHeld","Squad_GetSuppression","Squad_GetVeterancyExperience","Squad_GetVeterancyRank","Squad_GiveSlotItem","Squad_GiveSlotItemsFromTable","Squad_HasActiveCommand","Squad_HasAnyCritical","Squad_HasCritical","Squad_HasDestination","Squad_HasProductionQueue","Squad_HasSlotItem","Squad_HasTeamWeapon","Squad_HasUpgrade","Squad_IncreaseVeterancyExperience","Squad_IncreaseVeterancyRank","Squad_InstantSetupTeamWeapon","Squad_IsAttacking","Squad_IsCamouflaged","Squad_IsDoingAbility","Squad_IsFemale","Squad_IsHoldingAny","Squad_IsInCover","Squad_IsInHoldEntity","Squad_IsInHoldSquad","Squad_IsMoving","Squad_IsPinned","Squad_IsReinforcing","Squad_IsRetreating","Squad_IsSuppressed","Squad_IsUnderAttack","Squad_IsUnderAttackByPlayer","Squad_IsUnderAttackFromDirection","Squad_IsUpgrading","Squad_IsUpgradingAny","Squad_IsValid","Squad_Kill","Squad_RemoveAbility","Squad_RemoveUpgrade","Squad_RewardActionPoints","Squad_SetAnimatorState","Squad_SetAttackPlan","Squad_SetHealth","Squad_SetInvulnerable","Squad_SetInvulnerableEntityCount","Squad_SetInvulnerableMinCap","Squad_SetInvulnerableToCritical","Squad_SetMoodMode","Squad_SetMoveType","Squad_SetPinnedPlan","Squad_SetPlayerOwner","Squad_SetPosition","Squad_SetReactionPlan","Squad_SetRecrewable","Squad_SetRetaliationPlan","Squad_SetSharedProductionQueue","Squad_SetSuppression","Squad_SetVeterancyDisplayVisibility","Squad_SetWorldOwned","Squad_Spawn","Squad_SpawnToward","Squad_Split","Squad_StopAbility","Squad_SuggestPosture","Squad_WarpToPos","Stats_BuildingsLost","Stats_InfantryLost","Stats_KillsTotal","Stats_PlayerAt","Stats_PlayerCount","Stats_ResGathered","Stats_ResSpent","Stats_SoldiersKilled","Stats_StructuresKilled","Stats_TeamTally","Stats_TotalDuration","Stats_TotalSquadsLost","Stats_UnitSoldierKills","Stats_UnitStructureKills","Stats_UnitTotalKills","Stats_UnitVehicleKills","Stats_VehiclesKilled","Stats_VehiclesLost","Stinger_AddEvent","Stinger_AddFunction","Stinger_Remove","Team_AddResource","Team_AddSquadsToSGroup","Team_AreSquadsNearMarker","Team_CanSee","Team_ClearArea","Team_DefineAllies","Team_DefineEnemies","Team_FindByRace","Team_ForEachAllOrAny","Team_GetAll","Team_GetAllEntitiesNearMarker","Team_GetAllSquadsNearMarker","Team_GetBuildingID","Team_GetBuildingsCount","Team_GetBuildingsCountExcept","Team_GetBuildingsCountOnly","Team_GetEnemyTeam","Team_GetEntitiesFromType","Team_HasBuilding","Team_HasBuildingsExcept","Team_HasBuildingUnderConstruction","Team_IsAlive","Team_OwnsEGroup","Team_OwnsEntity","Team_OwnsSGroup","Team_OwnsSquad","Team_RestrictAddOnList","Team_RestrictBuildingList","Team_RestrictResearchList","Team_SetAbilityAvailability","Team_SetCommandAvailability","Team_SetConstructionMenuAvailability","Team_SetEntityProductionAvailability","Team_SetMaxCapPopulation","Team_SetMaxPopulation","Team_SetSquadProductionAvailability","Team_SetTechTreeByYear","Team_SetUpgradeAvailability","Team_SetUpgradeCost","ToW_DefenseCreateWave","ToW_SetStandardResources","ToW_SetUpBattleObjectives","ToW_SetUpTechTreeByYear","Timer_Add","Timer_Advance","Timer_Display","Timer_DisplayOnScreen","Timer_End","Timer_Exists","Timer_GetElapsed","Timer_GetMinutesAndSeconds","Timer_GetRemaining","Timer_IsPaused","Timer_Pause","Timer_Resume","Timer_Start","EventCue_Create","FOW_Enable","Game_SubTextFade","HintMouseover_Add","HintMouseover_Remove","HintPoint_Add","HintPoint_Remove","HintPoint_SetDisplayOffset","HintPoint_SetVisible","Misc_IsEGroupSelected","Misc_IsSGroupSelected","ThreatArrow_Add","ThreatArrow_CreateGroup","ThreatArrow_DestroyAllGroups","ThreatArrow_DestroyGroup","ThreatArrow_Remove","UI_AddHintAndFlashAbility","UI_CreateEventCue","UI_CreateMinimapBlip","UI_CreateSGroupKickerMessage","UI_DeleteMinimapBlip","UI_HighlightSGroup","UI_SetAllowLoadAndSave","UI_SetSGroupSpecialLevel","WinWarning_PublishLoseReminder","WinWarning_SetMaxTickers","WinWarning_SetTickers","WinWarning_ShowLoseWarning","Clone","Event_IsAnyRunning","Game_EndSP","Game_FadeToBlack","Import_Once","Loc_FormatText","Sound_PlayOnSquad","Team_GetEntityConcentration","Team_GetSquadConcentration","Util_AddMouseoverSquadToSGroup","Util_ApplyModifier","Util_AutoAmbient","Util_AutoIntel","Util_AutoNISlet","Util_Autosave","Util_ClearWrecksFromMarker","Util_DespawnAll","Util_DifVar","Util_ElementCanSee","Util_EntityLimit","Util_FallBackToGarrisonBuilding","Util_FindHiddenSpawn","Util_ForceRetreatAll","Util_GarrisonNearbyBuilding","Util_GarrisonNearbyVehicle","Util_GetClosestMarker","Util_GetEntitiesByBP","Util_GetHealth","Util_GetMouseoverSGroup","Util_GetPosition","Util_GetPositionAwayFromPlayer","Util_GetPositionFromAtoB","Util_GetRandomPosition","Util_GetSquadsByBP","Util_GetTrailingNumber","Util_HasPosition","Util_HidePlayerForNIS","Util_IsSequenceSkipped","Util_Kill","Util_LogSyncWpn","Util_MarkerFX","Util_MissionTitle","Util_MuteAmbientSound","Util_NewHUDFeatureEvent","Util_PlayMovie","Util_PlayMusic","Util_PrintObject","Util_ReinforceEvent","Util_ReloadScript","Util_RestoreMusic","Util_SetPlayerCanSkipSequence","Util_SetPlayerUnableToSkipSequence","Util_SortPositionsByClosest","Util_StartAmbient","Util_StartIntel","Util_StartNislet","Util_StartQuickIntel","Util_TableContains","Util_ToggleAllowIntelEvents","Util_TriggerEvent","Util_UnitCounts","World_KillAllNeutralEntitesNearMarker","Anim_PlayEntityAnim","bug","Camera_AutoRotate","Camera_ClampToMarker","Camera_FocusOnPosition","Camera_FollowEntity","Camera_FollowSelection","Camera_FollowSquad","Camera_GetCurrentTargetPos","Camera_GetDeclination","Camera_GetOrbit","Camera_GetTargetPos","Camera_GetTuningValue","Camera_GetZoomDist","Camera_IsInputEnabled","Camera_Reload","Camera_ResetFocus","Camera_ResetToDefault","Camera_SetDeclination","Camera_SetInputEnabled","Camera_SetOrbit","Camera_SetSlideTargetRate","Camera_SetTuningValue","Camera_SetZoomDist","Camera_StopAutoRotating","Camera_Unclamp","EGroup_CallEntityFunction","EGroup_CallEntityFunctionAllOrAny","fatal","Game_EnableInput","Game_EndSubTextFade","Game_EndTextTitleFade","Game_GetLocalPlayer","Game_GetMode","Game_GetSPDifficulty","Game_HasLocalPlayer","Game_IsLetterboxed","Game_IsPerformanceTest","Game_IsRTM","Game_Letterbox","Game_LoadAtmosphere","Game_LockRandom","Game_ProfileDumpFrames","Game_QuitApp","Game_ScreenFade","Game_SetLocalPlayer","Game_SetMode","Game_ShowPauseMenu","Game_SkipAllEvents","Game_SkipEvent","Game_StartMuted","Game_TextTitleFade","Game_TriggerLightning","Game_UnlockInputOnLetterBox","Game_UnLockRandom","Ghost_DisableSpotting","Ghost_EnableSpotting","HintPoint_AddToEGroup","HintPoint_AddToEntity","HintPoint_AddToPosition","HintPoint_AddToSGroup","HintPoint_AddToSquad","HintPoint_ClearFacing","HintPoint_RemoveAll","HintPoint_SetDisplayOffsetInternal","HintPoint_SetFacingEntity","HintPoint_SetFacingPosition","HintPoint_SetFacingSquad","HintPoint_SetVisibleInternal","inv_dump","IsOfType","IsSecuringStructure","IsStructure","License_CanPlayRace","LOC","Loc_ConvertNumber","Loc_Empty","Loc_FormatTime","Misc_AbortToFE","Misc_AddRestrictCommandsMarker","Misc_AIControlLocalPlayer","Misc_AreDefaultCommandsEnabled","Misc_DetectKeyboardInput","Misc_DetectMouseInput","Misc_DoWeaponHitEffectOnEntity","Misc_EnablePerformanceTest","Misc_GetCommandLineString","Misc_GetControlGroupContents","Misc_GetEntityControlGroup","Misc_GetHiddenPositionOnPath","Misc_GetMouseOnTerrain","Misc_GetMouseOverEntity","Misc_GetSelectedEntities","Misc_GetSelectedSquads","Misc_GetSquadControlGroup","Misc_IsCommandLineOptionSet","Misc_IsDevMode","Misc_IsEntityOnScreen","Misc_IsEntitySelected","Misc_IsMouseOverEntity","Misc_IsPosOnScreen","Misc_IsSelectionInputEnabled","Misc_IsSquadOnScreen","Misc_IsSquadSelected","Misc_RemoveCommandRestriction","Misc_RestrictCommandsToMarker","Misc_Screenshot","Misc_ScreenshotExt","Misc_SelectEntity","Misc_SelectSquad","Misc_SetDefaultCommandsEnabled","Misc_SetDesignerSplatsVisibility","Misc_SetEntityControlGroup","Misc_SetEntitySelectable","Misc_SetSelectionInputEnabled","Misc_SetSquadControlGroup","Misc_SetSquadSelectable","Mission_Complete","Mission_Fail","Mission_GetSecondaryObjective","Mission_StartBonusObjective","Mission_Win","Modifier_ApplyToEntity","Modifier_ApplyToPlayer","Modifier_ApplyToSquad","Modifier_Create","Modifier_Destroy","Modifier_IsEnabled","nis_setintransitiontime","nis_setouttransitionnis","nis_setouttransitiontime","Obj_Create","Obj_Delete","Obj_DeleteAll","Obj_GetState","Obj_GetVisible","Obj_HideProgress","Obj_SetDescription","Obj_SetIcon","Obj_SetObjectiveFunction","Obj_SetProgressBlinking","Obj_SetState","Obj_SetTitle","Obj_SetVisible","Obj_ShowProgress","Obj_ShowProgress2","Obj_ShowProgressTimer","OpBounty_AddRewardGroup","OpBounty_AddRewardTable","Order227_Init","PrintOnScreen","PrintOnScreen_Add","PrintOnScreen_Remove","PrintOnScreen_RemoveFromScreen","ResourceAmount_Add","ResourceAmount_ClampToZero","ResourceAmount_Has","ResourceAmount_Mult","ResourceAmount_Subtract","ResourceAmount_Sum","ResourceAmount_Zero","Scar_Autosave","Scar_CompleteIntelBulletinTask","Scar_DebugConsoleExecute","Scar_PlayNIS","Scar_PlayNIS2","Scar_ReloadAIScripts","Setup_GetVictoryPointTickerOption","Setup_SetPlayerName","Setup_SetPlayerRace","Setup_SetPlayerTeam","SGroup_CallEntityFunction","SGroup_CallSquadFunction","SGroup_CallSquadFunctionAllOrAny","SitRep_PlayMovie","SitRep_PlaySpeech","SitRep_StopMovie","Sound_ContainerDebug","Sound_DisableSpeechEvent","Sound_IsPlaying","Sound_PerfTest_Play2D","Sound_Play2D","Sound_Play3D","Sound_PlayMusic","Sound_PlayStreamed","Sound_PreCacheSinglePlayerSpeech","Sound_PreCacheSound","Sound_PreCacheSoundFolder","Sound_SetGlobalControlSource","Sound_SetMusicCombatValue","Sound_SetVolume","Sound_SetVolumeDefault","Sound_SetVolumeInv","Sound_StartRecording","Sound_Stop","Sound_StopAll","Sound_StopMusic","Sound_StopRecording","Speech_SetGlobalStealthRead","statgraph","statgraph_channel","statgraph_channel_get_enabled","statgraph_channel_set_enabled","statgraph_clear","statgraph_list","statgraph_pause","Subtitle_EndAllSpeech","Subtitle_EndCurrentSpeech","Subtitle_PlaySpeech","Subtitle_UnstickCurrentSpeech","SyncWeapon_CanAttackNow","SyncWeapon_Exists","SyncWeapon_GetEntity","SyncWeapon_GetFromEGroup","SyncWeapon_GetFromSGroup","SyncWeapon_GetPosition","SyncWeapon_IsAttacking","SyncWeapon_IsOwnedByPlayer","SyncWeapon_SetAutoTargetting","Taskbar_IsVisible","Taskbar_SetVisibility","TaskCountActivePBG","TaskCountPBG","UI_AutosaveMessageHide","UI_AutosaveMessageShow","UI_ClearEventCues","UI_ClearModalAbilityPhaseCallback","UI_ClearNISEndCallback","UI_CoverPreviewHide","UI_CoverPreviewShow","UI_CreateColouredEntityKickerMessage","UI_CreateColouredPositionKickerMessage","UI_CreateColouredSquadKickerMessage","UI_CreateEntityKickerMessage","UI_CreatePositionKickerMessage","UI_CreateSquadKickerMessage","UI_EnableGameEventCueType","UI_EnableResourceTypeKicker","UI_EnableUIEventCueType","UI_FlashAbilityButton","UI_FlashConstructionButton","UI_FlashConstructionMenu","UI_FlashEntity","UI_FlashEntityCommandButton","UI_FlashEventCue","UI_FlashObjectiveCounter","UI_FlashObjectiveIcon","UI_FlashProductionBuildingButton","UI_FlashProductionButton","UI_FlashSquadCommandButton","UI_GetDecoratorsEnabled","UI_HideTacticalMap","UI_HighlightSquad","UI_IsTacticalMapShown","UI_MessageBoxHide","UI_MessageBoxSetButton","UI_MessageBoxSetText","UI_NewHUDFeature","UI_OutOfBoundsLinesHide","UI_OutOfBoundsLinesShow","UI_RestrictBuildingPlacement","UI_ScreenFade","UI_SetAbilityCardVisibility","UI_SetAlliedBandBoxSelection","UI_SetCPMeterVisibility","UI_SetDecoratorsEnabled","UI_SetForceShowSubtitles","UI_SetModalAbilityPhaseCallback","UI_SetNISEndCallback","UI_SetSoviet227Blinking","UI_SetSoviet227Visibility","UI_ShowTacticalMap","UI_StopFlashing","UI_SystemMessageHide","UI_SystemMessageShow","UI_TerritoryHide","UI_TerritoryShow","UI_TitleDestroy","UI_ToggleDecorators","UI_UnrestrictBuildingPlacement","UIWarning_Show","Util_AddProxCheck","Util_ClearProxChecks","Util_CreateEntities","Util_CreateSquads","Util_GetDistance","Util_GetOffsetPosition","Util_GetPlayerOwner","Util_GetRelationship","Util_GetRelativeOffset","Util_MonitorTerritory","Util_RemoveProxCheck","Util_RemoveProxCheckByID","Util_ScarPos","Util_SetPlayerOwner","Util_SpawnDemoCharge","Util_StartNIS","VIS_OccCullToggleOBB","Marker_CleanUpTheDead","Weather_SetType","World_AddPilferLockArea","World_CleanUpTheDead","World_ClearCasualties","World_DamageIce","World_DestroyWallsNearMarker","World_DistanceEGroupToPoint","World_DistancePointToPoint","World_DistanceSGroupToPoint","World_DistanceSquaredPointToPoint","World_EnableReplacementObjectForEmptyPlayers","World_EnableSharedLineOfSight","World_EndSP","World_GetClosest","World_GetCurrentInteractionStage","World_GetEntitiesNearMarker","World_GetEntitiesNearPoint","World_GetEntitiesWithinTerritorySector","World_GetEntity","World_GetFurthest","World_GetGameTime","World_GetHeightAt","World_GetHiddenPositionOnPath","World_GetLength","World_GetNearestInteractablePoint","World_GetNeutralEntitiesNearMarker","World_GetNeutralEntitiesNearPoint","World_GetNeutralEntitiesWithinTerritorySector","World_GetNumEntities","World_GetNumEntitiesNearPoint","World_GetNumStrategicPoints","World_GetNumVictoryPoints","World_GetOffsetPosition","World_GetPlayerAt","World_GetPlayerCount","World_GetPlayerIndex","World_GetPossibleSquadsBlueprint","World_GetPossibleSquadsCount","World_GetRaceIndex","World_GetRand","World_GetSpawnablePosition","World_GetSquadsNearMarker","World_GetSquadsNearPoint","World_GetSquadsWithinTerritorySector","World_GetStrategyPoints","World_GetTeamTerritoryGaps","World_GetTeamVictoryTicker","World_GetTerritorySectorID","World_GetTerritorySectorPosition","World_GetWidth","World_IncreaseInteractionStage","World_IsGameOver","World_IsInSupply","World_IsPointInPlayerTerritory","World_IsTerritorySectorOwnedByPlayer","World_IsWinterMap","World_OwnsEGroup","World_OwnsEntity","World_OwnsSGroup","World_OwnsSquad","World_PointPointProx","World_Pos","World_RemoveAllResourcePoints","World_RemovePilferLockArea","World_SetDesignerSupply","World_SetGameOver","World_SetIceHealingRate","World_SetPlayerCustomSkin","World_SetPlayerLose","World_SetPlayerWin","World_SetSnowHealingRate","World_SetTeamWin","World_SpawnDemolitionCharge","World_TeamTerritoryPointsConnected","Scar_AddInit","scartype","scartype_tostring","import","UI_GetViewportWidth","UI_GetViewportHeight","UI_ButtonAdd","UI_ButtonSetCallback","UI_ButtonSetEnabled","UI_ButtonSetIcon","UI_ButtonSetTag","UI_ButtonSetText","UI_LabelAdd","UI_LabelSetText","UI_IconAdd","UI_IconSetIcon","UI_PanelAdd","UI_StatusIndicatorAdd","UI_StatusIndicatorSetValue","UI_ControlSetColour","UI_ControlSetPosition","UI_ControlSetRect","UI_ControlRemove","UI_ControlClear","BS_NearBase","BS_Defend","BS_Secure","BS_Mines","BS_OuterBase","CPT_VictoryPoint","CPT_MunitionPoint","CPT_NullPoint","CPT_TacticalPoint","CPT_INVALID","CPT_FuelPoint","COMBAT_Default","COMBAT_Defend","COMBAT_Attack","MPT_VictoryPoint","MPT_NullPoint","MPT_NONE","MPT_MunitionPoint","MPT_COUNT","MPT_SupportStructure","MPT_Defence","MPT_Spawner","MPT_HQ","MPT_TacticalPoint","MPT_FuelPoint","MTARGET_Attack","MTARGET_Defend","AI_ProductionQueue","AI_CapturePoint","AI_Squad","AITacticTargetPreference_HighDamage","AITacticTargetPreference_LowHealth","AITacticTargetPreference_None","AITacticTargetPreference_Support","AITacticTargetPreference_Near","AITacticTargetPreference_NearAndBest","AITacticTargetPreference_Best","TACTIC_CapturePoint","TACTIC_Ability","TACTIC_Pickup","TACTIC_ForceAttack","TACTIC_Hold","TACTIC_MinRange","TACTIC_CaptureTeamWeapon","TACTIC_WarmUp","TACTIC_ProvideReinforcementPoint","TACTIC_RushAtTarget","TACTIC_Recrew","TACTIC_Vehicle","TACTIC_Avoid","TACTIC_Cover","TACTIC_FinishHealing","TASK_Leader","TASK_Production","TASK_Ability","TASK_PlayerAbility","TASK_Combat","TASK_Construction","TASK_Capture","TASK_ImmobileCombat","AII_LocalHumanTakeover","AII_RemoteAITakeover","AII_None","AII_RemoteHumanTakeover","AII_Normal","ITEM_REMOVED","ITEM_DEFAULT","ITEM_UNLOCKED","ITEM_LOCKED","BT_AttackHere","BT_SectorArtillery","BT_ObjectivePrimary","BT_Reveal","BT_Combat","BT_General","BT_CaptureHere","BT_DefendHere","BT_ObjectiveSecondary","BT_RallyPoint","BFS_Smoking","BFS_Burning","BFS_NotOnFire","TV_DeclinationEnabled","TV_DistMaxDead","TV_DistRateMouse","TV_NISletDistMin","TV_SlideOrbitRate","TV_PanScaleKeyboardDefZ","TV_PanScaleMouseDefZ","TV_SlideDeclThreshold","TV_PanStartSpeedScalar","TV_EntityMinViewAngle","TV_SlideTargetBase","TV_NearPlaneShifter","TV_DistMin","TV_PanScaleScreenDefZ","TV_NISletDistGroundMin","TV_DeclBelow","TV_SlideTargetThreshold","TV_DeclAbove","TV_DistScale","TV_NISletDistMax","TV_PanMaxSpeedScalar","TV_NISletDeclAbove","TV_NISletDistMinGround","TV_ZoomLocked","TV_CameraMode","TV_DefaultAngle","TV_PanScaleKeyboardMinZ","TV_PanScaleMouseMinZ","TV_DeclBelowClose","TV_TrackElastic","TV_DistExpWheel","TV_DistExpMouse","TV_DistMinGround","TV_DistGroundTargetHeight","TV_ClipFar","TV_DistGroundMin","TV_DistMinDead","TV_DistMax","TV_SlideDeclBase","TV_SlideOrbitThreshold","TV_SlideOrbitBase","TV_SlideDistThreshold","TV_SlideDistBase","TV_SlideTargetRate","TV_ClipNear","TV_PanScaleScreenMinZ","TV_DistRateWheelZoomIn","TV_SlideDistRate","TV_DistRateWheelZoomOut","TV_TrackBoundScale","TV_DefaultDeclination","TV_PanAccelerate","TV_DeclRateMouse","TV_DistExp","TV_DefaultHeight","TV_SlideDeclRate","TV_RotationEnabled","TV_OrbitRateMouse","TV_FieldOfView","TV_NISletDeclBelow","CANPRODUCE_PrerequisitesProducer","CANPRODUCE_Error","CANPRODUCE_ProductionQueueFull","CANPRODUCE_ProductionItemFull","CANPRODUCE_OutOfReinforceRadius","CANPRODUCE_Ok","CANPRODUCE_Disabled","CANPRODUCE_OutOfTerritory","CANPRODUCE_UpgradeItemFull","CANPRODUCE_PopulationCapFull","CANPRODUCE_NoResources","CANPRODUCE_PrerequisitesItem","CANPRODUCE_NoItem","CT_Medic","CT_Vehicle","CT_Personnel","CHECK_BOTH","CHECK_OFFCAMERA","CHECK_IN_FOW","CT_VehicleOpticsDamaged","CT_VehicleExhaustDamaged","CT_VehicleKillCommander","CT_VehicleDriverInjured","CT_VehicleEngineYellow","CT_VehicleBack","CT_VehicleLeft","CT_VehicleRight","CT_VehicleGunnerInjured","CT_VehicleEngineGreen","CT_VehicleCrewShocked","CT_VehicleFront","CT_VehicleEngineBurning","CT_VehicleEngineRed","CT_VehicleSecondaryWeapon","CT_VehicleLoseTreadsOrWheels","CT_VehicleOutOfControl","CT_VehiclePrimaryWeapon","Crush_Heavy","Crush_Off","Crush_Light","Crush_Medium","DB_Button3","DB_Button1","DB_Close","DB_Button2","CMD_InstantBuildSquad","CMD_InstantDeath","CMD_AttackStop","CMD_BuildStructure","CMD_Face","CMD_CancelProduction","CMD_RescueCasualty","CMD_SetHoldHeading","CMD_DefuseMine","CMD_AttackMove","CMD_Fidget","CMD_Stop","CMD_PlaceCharge","CMD_Paradrop","CMD_Destroy","CMD_Load","CMD_Ability","CMD_Move","CMD_InstantUpgrade","CMD_UnloadSquads","CMD_Casualty","CMD_BuildSquad","CMD_Halt","CMD_Attack","CMD_Capture","CMD_AttackForced","CMD_Death","CMD_Unload","CMD_Evacuate","CMD_BuildEntity","CMD_Vault","CMD_AttackFromHold","CMD_RallyPoint","CMD_DefaultAction","CMD_Upgrade","CMD_ChooseResource","CMD_Projectile","STATEID_Capture","STATEID_Idle","STATEID_Evacuate","STATEID_StructureBuilding","STATEID_RepairEngineer","STATEID_Move","STATEID_Dead","STATEID_DefuseMine","GE_ProjectileFired","GE_AIPlayer_Migrated","GE_EntityKilled","GE_TerritoryEntered","GE_ConstructionComplete","GE_NonGlobalCamoDetected","GE_SquadPinned","GE_BuildItemComplete","GE_PlayerKilled","GE_EntityCommandIssued","GE_StrategicPointChanged","GE_PlayerDonation","GE_AbilityExecuted","GE_PlayerDropped","GE_PlayerBeingAttacked","GE_UpgradeComplete","GE_PlayerSkipNIS","GE_AIPlayer_ObjectiveNotification","GE_ResourceDepleted","GE_CustomUIEvent","GE_SquadKilled","GE_PlayerSurrendered","GE_SquadCommandIssued","GE_EntityParadropComplete","GE_PlayerCheat","GE_InfoPointActivated","GE_SpawnActionComplete","GE_PlayerCommandIssued","GE_PlayerHostMigrated","GE_SquadParadropComplete","GE_PlayerPhaseUp","HPAT_Hint","HPAT_MovementLooping","HPAT_Bonus","HPAT_Vaulting","HPAT_Detonation","HPAT_CoverRed","HPAT_CoverYellow","HPAT_Artillery","HPAT_FormationSetup","HPAT_Movement","HPAT_Critical","HPAT_Objective","HPAT_AttackLooping","HPAT_DeepSnow","HPAT_CoverGreen","HPAT_Attack","HPAT_RallyPoint","HUDF_None","HUDF_AbilityCard","HUDF_Upgrades","HUDF_CommandCard","HUDF_MiniMap","LOOP_NORMAL","LOOP_TOGGLE_DIRECTION","LOOP_NONE","MAP_Confirmed","MAP_Placing","MAP_Facing","MAT_Entity","MAT_Player","MAT_Weapon","MAT_Upgrade","MAT_EntityType","MAT_Ability","MAT_Squad","MAT_WeaponType","MAT_SquadType","MUT_Multiplication","MUT_MultiplyAdd","MUT_Addition","MUT_Enable","PBG_Weapon","PBG_MoveType","PBG_SlotItem","PBG_UITacticalMap","PBG_HitMaterial","PBG_PassType","PBG_Race","PBG_UISelection","PBG_Critical","PBG_CamouflageStance","PBG_Material","PBG_Tuning","PBG_Ability","PBG_Upgrade","PBG_Posture","PBG_UITerritory","MM_ForceTense","MM_ForceCalm","MM_Auto","FN_OnShow","FN_OnCounterDisplay","FN_OnActivate","FN_LuaTableQuery","FN_OnSelect","OS_Complete","OS_Incomplete","OS_Off","OS_Failed","OT_Secondary","OT_Primary","OT_Ally","OT_Neutral","OT_Player","OT_Enemy","PCMD_MunitionDonation","PCMD_SlotItemRemove","PCMD_CriticalHit","PCMD_CheatBuildTime","PCMD_Ability","PCMD_SetCommander","PCMD_CheatRevealAll","PCMD_ManpowerDonation","PCMD_UpgradeRemove","PCMD_ConstructField","PCMD_CancelProduction","PCMD_CheatKillSelf","PCMD_Upgrade","PCMD_ConstructFence","PCMD_FuelDonation","PCMD_DetonateCharges","PCMD_CheatResources","PCMD_AIPlayer","PCMD_AIPlayer_ObjectiveNotification","PCMD_ConstructStructure","PCMD_InstantUpgrade","PITEM_SquadUpgrade","PITEM_SquadReinforce","PITEM_Spawn","PITEM_Upgrade","PT_Rectangle","PT_Circle","R_NEUTRAL","R_ENEMY","R_UNDEFINED","R_ALLY","RT_SovietOrder227","RT_Command","RT_SovietProgression","RT_Popcap","RT_Manpower","RT_Munition","RT_Fuel","RT_Action","RUIITEM_Population","RUIITEM_ResourceBar","RUIITEM_Munitions","RUIITEM_Manpower","RUIITEM_Fuel","ST_MARKER","ST_PBG","ST_SCARPOS","ST_AIPLAYER","ST_TABLE","ST_EGROUP","ST_AISTATSMILITARYPOINT","ST_AISQUAD","ST_ENTITY","ST_NUMBER","ST_FUNCTION","ST_SQUAD","ST_PLAYER","ST_BOOLEAN","ST_NIL","ST_CONSTPLAYER","ST_UNKNOWN","ST_SGROUP","ST_STRING","ST_AICAPTUREPOINT","PBG_TurnPlan","PBG_EntityProperties","PBG_SquadFormation","PBG_SquadProperties","PBG_Formation","DEBUG_SELECTOR","DEBUG_COMBATZONES","SCMD_Attack","SCMD_Upgrade","SCMD_StationaryAttack","SCMD_SlotItemRemove","SCMD_Pilfer","SCMD_SetMoveType","SCMD_Ability","SCMD_Move","SCMD_BuildStructure","SCMD_InstantLoad","SCMD_Merge","SCMD_UnloadSquads","SCMD_Retreat","SCMD_DefaultAction","SCMD_RescueCasualty","SCMD_Stop","SCMD_SetCamouflageStance","SCMD_AttackMove","SCMD_RevertFieldSupport","SCMD_CancelProduction","SCMD_Capture","SCMD_Surprise","SCMD_ReinforceUnit","SCMD_CaptureTeamWeapon","SCMD_Patrol","SCMD_Face","SCMD_Recrew","SCMD_DoPlan","SCMD_DefuseCharge","SCMD_PickUpSlotItem","SCMD_BuildSquad","SCMD_InstantReinforceUnit","SCMD_Load","SCMD_InstantSetupTeamWeapon","SCMD_RallyPoint","SCMD_AbandonTeamWeapon","SCMD_Unload","SCMD_DefuseMine","SCMD_Destroy","SCMD_PlaceCharge","SCMD_InstantUpgrade","SQUADSTATEID_Capture","SQUADSTATEID_CaptureTeamWeapon","SQUADSTATEID_Move","SQUADSTATEID_Retreat","SQUADSTATEID_Plan","SQUADSTATEID_AttackMove","SQUADSTATEID_Load","SQUADSTATEID_Defuse","SQUADSTATEID_DefuseMine","SQUADSTATEID_Stop","SQUADSTATEID_Patrol","SQUADSTATEID_Ability","SQUADSTATEID_CombatStance","SQUADSTATEID_RevertFieldSupport","SQUADSTATEID_Unload","SQUADSTATEID_HoldUnload","SQUADSTATEID_PickUpSlotItem","SQUADSTATEID_Construction","SQUADSTATEID_Idle","SQUADSTATEID_WeaponTransition","SQUADSTATEID_Recrew","SQUADSTATEID_PlaceCharges","SQUADSTATEID_Combat","UIE_UpgradeComplete","UIE_PlayerPingOfShameLocal","UIE_EnemyReveal","UIE_InfoPointActivated","UIE_AITakeOver","UIE_VehicleComplete","UIE_AllyAttacked","UIE_CommanderAbilityUnlocked","UIE_CommandersUnlocked","UIE_CommandPointGained","UIE_SquadFreezing","UIE_SquadCold","UIE_CasualtySquadSpawned","UIE_SquadVeterancy","UIE_VehicleReplaced","UIE_InfantryReplaced","UIE_Sniped","UIE_BoobyTrap","UIE_MineDetected","UIE_AbilityExectued","UIE_StrategicPointCaptured","UIE_StrategicPointReverting","UIE_EnemyTerritoryEntered","UIE_TerritoryEntered","UIE_PlayerSurrendered","UIE_PlayerAttacked","UIE_VehicleAttacked","UIE_PlayerKilled","UIE_PlayerKicked","UIE_PlayerLagComplaint","UIE_PlayerPingOfShame","UIE_PlayerDropped","UIE_ConstructionComplete","UIE_StrategicPointSecured","UIE_ResourceDepleted","UIE_SquadPinned","UIE_InfantryAttacked","UIE_InfantryComplete","UIE_PlayerCheated","UIE_PhaseUp","UIE_HostMigrated","UIE_Default","UI_Cinematic","UI_Fullscreen","UI_Normal","UOT_Player","UOT_Self","UOT_None","BIS_Icon","BIS_IconState","LAH_Justify","LAH_Left","LAH_Center","LAH_Right","LAV_None","LAV_Top","LAV_Center","LAV_Bottom","assert","collectgarbage","dofile","error","getmetatable","ipairs","load","loadfile","next","pairs","pcall","print","rawequal","rawget","rawlen","rawset","select","setmetatable","tonumber","tostring","type","xpcall","string.byte","string.char","string.dump","string.find","and","break","do","else","elseif","end","false","for","function","if","in","local","nil","not","or","repeat","return","then","true","until","while","math.huge","math.maxinteger","math.mininteger","math.pi","EBP.WRECKED_VEHICLES.FRONT_HULL01","EBP.WRECKED_VEHICLES.FROZEN_PANZER_IV","EBP.WRECKED_VEHICLES.FROZEN_STUG_III","EBP.WRECKED_VEHICLES.HORSA_COCKPIT","EBP.WRECKED_VEHICLES.HORSA_FRONT_HULL","EBP.WRECKED_VEHICLES.HORSA_LEFT_WING","EBP.WRECKED_VEHICLES.HORSA_LEFT_WING_TIP","EBP.WRECKED_VEHICLES.HORSA_MID_HULL","EBP.WRECKED_VEHICLES.HORSA_REAR_HULL","EBP.WRECKED_VEHICLES.HORSA_RIGHT_WING","EBP.WRECKED_VEHICLES.HORSA_RIGHT_WING_TIP","EBP.WRECKED_VEHICLES.HORSA_TAIL","EBP.WRECKED_VEHICLES.LEFT_WING","EBP.WRECKED_VEHICLES.MAP_OBJECT_M4SHERMAN_105MM","EBP.WRECKED_VEHICLES.MAP_OBJECT_M4SHERMAN_76MM","EBP.WRECKED_VEHICLES.MAP_OBJECT_M4SHERMAN_DOZER","EBP.WRECKED_VEHICLES.MAP_OBJECT_OPELBLITZ","EBP.WRECKED_VEHICLES.MAP_OBJECT_PAK38","EBP.WRECKED_VEHICLES.MAP_OBJECT_PANZERIV","EBP.WRECKED_VEHICLES.MAP_OBJECT_STUGIII_LONG","EBP.WRECKED_VEHICLES.MAP_OBJECT_STUGIII_SHORT","EBP.WRECKED_VEHICLES.PROPELLER","EBP.WRECKED_VEHICLES.RIGHT_WING","EBP.WRECKED_VEHICLES.STUKA_BODY","EBP.WRECKED_VEHICLES.STUKA_DEBRIS","EBP.WRECKED_VEHICLES.STUKA_TAIL","EBP.WRECKED_VEHICLES.STUKA_WING_LEFT","EBP.WRECKED_VEHICLES.STUKA_WING_RIGHT","EBP.WRECKED_VEHICLES.TAIL","EBP.WRECKED_VEHICLES.TAIL_SECTION_01","EBP.WRECKED_VEHICLES.WRECKED_50MM_PAK38_MAP_OBJECT","EBP.WRECKED_VEHICLES.WRECKED_ARMORED_CAR_PUMA_MP","EBP.WRECKED_VEHICLES.WRECKED_ARMORED_CAR_SDKFZ_222","EBP.WRECKED_VEHICLES.WRECKED_ARMORED_CAR_SDKFZ_222_MP","EBP.WRECKED_VEHICLES.WRECKED_ARMORED_CAR_SDKFZ_234","EBP.WRECKED_VEHICLES.WRECKED_ARMORED_CAR_SDKFZ_234_BREW_UP","EBP.WRECKED_VEHICLES.WRECKED_ARMORED_CAR_SDKFZ_234_PUMA_MP","EBP.WRECKED_VEHICLES.WRECKED_ATGUN_17_POUNDER","EBP.WRECKED_VEHICLES.WRECKED_ATGUN_45MM","EBP.WRECKED_VEHICLES.WRECKED_ATGUN_75MM_PAK","EBP.WRECKED_VEHICLES.WRECKED_ATGUN_B4_200MM","EBP.WRECKED_VEHICLES.WRECKED_ATGUN_M1_57MM","EBP.WRECKED_VEHICLES.WRECKED_ATGUN_ML20","EBP.WRECKED_VEHICLES.WRECKED_ATGUN_PAK43","EBP.WRECKED_VEHICLES.WRECKED_ATGUN_ZIS3","EBP.WRECKED_VEHICLES.WRECKED_BASE_BUILDING01","EBP.WRECKED_VEHICLES.WRECKED_BASE_BUILDING01_SELF_DESTRUCT","EBP.WRECKED_VEHICLES.WRECKED_BASE_BUILDING02","EBP.WRECKED_VEHICLES.WRECKED_BASE_BUILDING02_SELF_DESTRUCT","EBP.WRECKED_VEHICLES.WRECKED_BASE_BUILDING03","EBP.WRECKED_VEHICLES.WRECKED_BASE_BUILDING03_SELF_DESTRUCT","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_AEC","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_AEC_ARMOURED_CAR_BREW_UP","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_ATGUN_6_POUNDER","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_BOFORS","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_CENTAUR","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_CHURCHILL","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_CHURCHILL_AVRE","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_CHURCHILL_AVRE_BREW_UP","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_CHURCHILL_BREW_UP","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_CHURCHILL_CROCODILE","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_CHURCHILL_CROCODILE_BREW_UP","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_COMET","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_COMET_BREW_UP","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_CROMWELL","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_CROMWELL_BREW_UP","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_GLIDER_HQ_MP","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_GLIDER_MP","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_SEXTON","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_SHERMAN_FIREFLY","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_SHERMAN_FIREFLY_BREW_UP","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_UNIVERSAL_CARRIER","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_VALENTINE_COMMAND","EBP.WRECKED_VEHICLES.WRECKED_BRITISH_VALENTINE_COMMAND_BREW_UP","EBP.WRECKED_VEHICLES.WRECKED_BRUMMBAR_02","EBP.WRECKED_VEHICLES.WRECKED_BRUMMBAR_STURMPANZER_IV_SDKFZ_166","EBP.WRECKED_VEHICLES.WRECKED_EARLY_WAR_TANK_01","EBP.WRECKED_VEHICLES.WRECKED_ELEFANT_SDKFZ_184","EBP.WRECKED_VEHICLES.WRECKED_FN63_4RM","EBP.WRECKED_VEHICLES.WRECKED_HALFTRACK_SDKFZ_250","EBP.WRECKED_VEHICLES.WRECKED_HALFTRACK_SDKFZ_250_MORTAR","EBP.WRECKED_VEHICLES.WRECKED_HALFTRACK_SDKFZ_251","EBP.WRECKED_VEHICLES.WRECKED_HALFTRACK_SDKFZ_251_17_FLAK","EBP.WRECKED_VEHICLES.WRECKED_HALFTRACK_SDKFZ_251_INFRARED","EBP.WRECKED_VEHICLES.WRECKED_HALFTRACK_SDKFZ_251_MP","EBP.WRECKED_VEHICLES.WRECKED_HALFTRACK_SDKFZ_251_WALKING_STUKA","EBP.WRECKED_VEHICLES.WRECKED_HALFTRACK_SWS","EBP.WRECKED_VEHICLES.WRECKED_HETZER","EBP.WRECKED_VEHICLES.WRECKED_HETZER_BREWUP","EBP.WRECKED_VEHICLES.WRECKED_HOWITZER_105MM_MAP_OBJECT","EBP.WRECKED_VEHICLES.WRECKED_IG18_SUPPORT_GUN","EBP.WRECKED_VEHICLES.WRECKED_IS_2_HEAVY_TANK","EBP.WRECKED_VEHICLES.WRECKED_ISU_152_SPG","EBP.WRECKED_VEHICLES.WRECKED_JAGDPANZER_IV","EBP.WRECKED_VEHICLES.WRECKED_JAGDPANZER_IV_BREW_UP","EBP.WRECKED_VEHICLES.WRECKED_JAGDTIGER_TD","EBP.WRECKED_VEHICLES.WRECKED_JAGDTIGER_TD_BREW_UP","EBP.WRECKED_VEHICLES.WRECKED_KATYUSHA_BM_13N","EBP.WRECKED_VEHICLES.WRECKED_KATYUSHA_BM_13N_MP","EBP.WRECKED_VEHICLES.WRECKED_KING_TIGER","EBP.WRECKED_VEHICLES.WRECKED_KING_TIGER_BREW_UP","EBP.WRECKED_VEHICLES.WRECKED_KUBELWAGEN","EBP.WRECKED_VEHICLES.WRECKED_KV_1","EBP.WRECKED_VEHICLES.WRECKED_KV_1_MP","EBP.WRECKED_VEHICLES.WRECKED_KV_2","EBP.WRECKED_VEHICLES.WRECKED_KV_8","EBP.WRECKED_VEHICLES.WRECKED_LAND_MATTRESS","EBP.WRECKED_VEHICLES.WRECKED_M10","EBP.WRECKED_VEHICLES.WRECKED_M10_BREW_UP","EBP.WRECKED_VEHICLES.WRECKED_M15A1_AA_HALFTRACK","EBP.WRECKED_VEHICLES.WRECKED_M15A1_AA_HALFTRACK_MAP_OBJECT","EBP.WRECKED_VEHICLES.WRECKED_M20_UTILITY_CAR","EBP.WRECKED_VEHICLES.WRECKED_M21_MORTAR_HALFTRACK","EBP.WRECKED_VEHICLES.WRECKED_M26_PERSHING","EBP.WRECKED_VEHICLES.WRECKED_M3_HALFTRACK","EBP.WRECKED_VEHICLES.WRECKED_M36","EBP.WRECKED_VEHICLES.WRECKED_M36_BREW_UP","EBP.WRECKED_VEHICLES.WRECKED_M3A1_SCOUT_CAR","EBP.WRECKED_VEHICLES.WRECKED_M3A1_SCOUT_CAR_MP","EBP.WRECKED_VEHICLES.WRECKED_M4A3_SHERMAN","EBP.WRECKED_VEHICLES.WRECKED_M4A3_SHERMAN_BREW_UP","EBP.WRECKED_VEHICLES.WRECKED_M4A3_SHERMAN_BULLDOZER","EBP.WRECKED_VEHICLES.WRECKED_M4A3_SHERMAN_BULLDOZER_BREW_UP","EBP.WRECKED_VEHICLES.WRECKED_M4A3_SHERMAN_EASY_EIGHT","EBP.WRECKED_VEHICLES.WRECKED_M4A3_SHERMAN_EASY_EIGHT_BREW_UP","EBP.WRECKED_VEHICLES.WRECKED_M4A3_SHERMAN_MAP_OBJECT","EBP.WRECKED_VEHICLES.WRECKED_M5_HALFTRACK","EBP.WRECKED_VEHICLES.WRECKED_M5_HALFTRACK_MP","EBP.WRECKED_VEHICLES.WRECKED_M5A1_STUART","EBP.WRECKED_VEHICLES.WRECKED_M8_ARMORED_CAR","EBP.WRECKED_VEHICLES.WRECKED_M8_HMC","EBP.WRECKED_VEHICLES.WRECKED_OPEL_BLITZ_TRUCK","EBP.WRECKED_VEHICLES.WRECKED_OSTWIND_FLAK_PANZER","EBP.WRECKED_VEHICLES.WRECKED_PACK_HOWITZER","EBP.WRECKED_VEHICLES.WRECKED_PANTHER_MAP_OBJECT","EBP.WRECKED_VEHICLES.WRECKED_PANTHER_SDKFZ_171","EBP.WRECKED_VEHICLES.WRECKED_PANTHER_SDKFZ_171_BREWUP","EBP.WRECKED_VEHICLES.WRECKED_PANZER_II_LUCHS","EBP.WRECKED_VEHICLES.WRECKED_PANZER_II_LUCHS_BREW_UP","EBP.WRECKED_VEHICLES.WRECKED_PANZER_III","EBP.WRECKED_VEHICLES.WRECKED_PANZER_IV_FROZEN","EBP.WRECKED_VEHICLES.WRECKED_PANZER_IV_SDKFZ_161","EBP.WRECKED_VEHICLES.WRECKED_PANZER_IV_SDKFZ_161_COMMAND","EBP.WRECKED_VEHICLES.WRECKED_PANZER_IV_SDKFZ_161_GAMEPLAY","EBP.WRECKED_VEHICLES.WRECKED_PANZER_IV_SDKFZ_161_WEST_GERMAN","EBP.WRECKED_VEHICLES.WRECKED_PANZER_IV_SDKFZ_161_WEST_GERMAN_BREW_UP","EBP.WRECKED_VEHICLES.WRECKED_PANZERIV_MAP_OBJECT","EBP.WRECKED_VEHICLES.WRECKED_PANZERWERFER_SDKFZ_4_1","EBP.WRECKED_VEHICLES.WRECKED_PRIEST","EBP.WRECKED_VEHICLES.WRECKED_RAKETENWERFER","EBP.WRECKED_VEHICLES.WRECKED_SOVIET_76MM_SHERMAN","EBP.WRECKED_VEHICLES.WRECKED_STUG_III_E_SDKFZ_141_1","EBP.WRECKED_VEHICLES.WRECKED_STUG_III_FROZEN","EBP.WRECKED_VEHICLES.WRECKED_STUG_III_G_SDKFZ_141_1","EBP.WRECKED_VEHICLES.WRECKED_STUG_III_G_SDKFZ_141_1_GAMEPLAY","EBP.WRECKED_VEHICLES.WRECKED_STURMTIGER","EBP.WRECKED_VEHICLES.WRECKED_SU_76M","EBP.WRECKED_VEHICLES.WRECKED_SU_85","EBP.WRECKED_VEHICLES.WRECKED_T_34_76","EBP.WRECKED_VEHICLES.WRECKED_T_34_76_02","EBP.WRECKED_VEHICLES.WRECKED_T_34_76_MP","EBP.WRECKED_VEHICLES.WRECKED_T_34_85_RED_BANNER","EBP.WRECKED_VEHICLES.WRECKED_T_34_85_RED_BANNER_MP","EBP.WRECKED_VEHICLES.WRECKED_T_34_85_RED_BANNER_TOW","EBP.WRECKED_VEHICLES.WRECKED_T34_CALLIOPE","EBP.WRECKED_VEHICLES.WRECKED_T70","EBP.WRECKED_VEHICLES.WRECKED_T70_MP","EBP.WRECKED_VEHICLES.WRECKED_TIGER_SDKFZ_181","EBP.WRECKED_VEHICLES.WRECKED_TIGER_SDKFZ_181_SINGLEPLAYER_MISSION","EBP.WRECKED_VEHICLES.WRECKED_WC51","EBP.WRECKED_VEHICLES.WRECKED_WC54_AMBULANCE","EBP.AEF.AEF_AIRDROPPED_MINE_CONTACT_MP","EBP.AEF.AEF_AIRDROPPED_MINE_MP","EBP.AEF.AEF_ALLIEDSUPPLY_STACK_L_01_MP","EBP.AEF.AEF_ATTACK_PLANE","EBP.AEF.AEF_BARBED_WIRE_FENCE_MP","EBP.AEF.AEF_BARRACKS","EBP.AEF.AEF_BASE_STAMPER","EBP.AEF.AEF_GARRISON","EBP.AEF.AEF_MG_NEST","EBP.AEF.AEF_MG_NEST_AEF_BASE","EBP.AEF.AEF_MG_NEST_PERIMETER_MP","EBP.AEF.AEF_MINE_MP","EBP.AEF.AEF_MINE_RIFLEMEN_MP","EBP.AEF.AEF_SANDBAG_DIRTWALL_01","EBP.AEF.AEF_SANDBAG_FENCE","EBP.AEF.AEF_SANDBAGS","EBP.AEF.AEF_SANDBAGWALL","EBP.AEF.AEF_SANDBAGWALL_COVER_SPECIALIZATION","EBP.AEF.AEF_STORAGEBUNKER","EBP.AEF.AEF_SUPPLYTENT","EBP.AEF.AEF_TANK_TRAP_IMPASSABLE_MP","EBP.AEF.AEF_TANK_TRAP_MP","EBP.AEF.AEF_WEAPON_RACK_BAZOOKA_MP","EBP.AEF.AEF_WEAPON_RACK_BROWNING_AUTOMATIC_RIFLE_MP","EBP.AEF.AEF_WEAPON_RACK_DEFAULT_MP","EBP.AEF.AEF_WEAPON_RACK_M1919_LMG","EBP.AEF.AEF_WEAPON_RACK_M1C_GARAND","EBP.AEF.AEF_WEAPON_RACK_M9_BAZOOKA_MP","EBP.AEF.AIRBORNE_BEACON_MP","EBP.AEF.ARMOR_COMMAND_MP","EBP.AEF.ARMOR_COMMAND_SP","EBP.AEF.ARMOR_COMMAND_WRECK_MP","EBP.AEF.ARMORED_RIFLE_COMMAND_MP","EBP.AEF.ARMORED_RIFLE_COMMAND_SP","EBP.AEF.ARMORED_RIFLE_COMMAND_WRECK_MP","EBP.AEF.ASSAULT_ENGINEER_MP","EBP.AEF.ASSAULT_ENGINEER_VEHICLE_CREW_MP","EBP.AEF.AT_TEAM_WEAPON_CREW_MP","EBP.AEF.CAPTAIN_MP","EBP.AEF.CAPTAIN_UNLOCK_MP","EBP.AEF.COMPANY_WEAPONS_POOL_MP","EBP.AEF.COMPANY_WEAPONS_POOL_SP","EBP.AEF.COMPANY_WEAPONS_POOL_WRECK_MP","EBP.AEF.DODGE_WC51_50CAL_MP","EBP.AEF.DODGE_WC51_50CAL_PARADROP","EBP.AEF.DODGE_WC51_AMBULANCE_MP","EBP.AEF.DODGE_WC51_MP","EBP.AEF.DODGE_WC51_MP_PATHFINDERS","EBP.AEF.FIGHTING_POSITION_MP","EBP.AEF.FIGHTING_POSITION_RIFLEMEN_MP","EBP.AEF.HMG_TEAM_WEAPON_CREW_MP","EBP.AEF.HOWITZER_TEAM_WEAPON_CREW_MP","EBP.AEF.INVISI_HEAL_STATION_MP","EBP.AEF.INVISI_REPAIR_STATION_MP","EBP.AEF.JACKSON","EBP.AEF.LIEUTENANT_MP","EBP.AEF.LIEUTENANT_UNLOCK_MP","EBP.AEF.M1_57MM_ANTITANK_GUN_MP","EBP.AEF.M1_75MM_PACK_HOWITZER_MP","EBP.AEF.M1_81MM_MORTAR_MP","EBP.AEF.M10_TANK_DESTROYER_MP","EBP.AEF.M15A1_AA_HALFTRACK_MP","EBP.AEF.M1919A4_30CAL_MACHINE_GUN_MP","EBP.AEF.M1919A4_TEAM_WEAPON_CREW_MP","EBP.AEF.M2_60MM_MORTAR_MP","EBP.AEF.M20_M6_AT_MINE_MP","EBP.AEF.M20_UTILITY_CAR_MP","EBP.AEF.M21_MORTAR_HALFTRACK_MP","EBP.AEF.M26_PERSHING_MP","EBP.AEF.M2HB_50CAL_MACHINE_GUN_MP","EBP.AEF.M3_HALFTRACK_ASSAULT_MP","EBP.AEF.M3_HALFTRACK_MP","EBP.AEF.M36_TANK_DESTROYER_MP","EBP.AEF.M4A3_76MM_SHERMAN_MP","EBP.AEF.M4A3_SHERMAN_BULLDOZER_MP","EBP.AEF.M4A3_SHERMAN_DEMO_BURNOUT","EBP.AEF.M4A3_SHERMAN_MP","EBP.AEF.M4A3E8_SHERMAN_EASY_8_MP","EBP.AEF.M5_HALFTRACK_USF_MP","EBP.AEF.M5A1_STUART_MP","EBP.AEF.M7B1_PRIEST_MP","EBP.AEF.M8_GREYHOUND_MP","EBP.AEF.M8A1_HMC_MP","EBP.AEF.MAJOR_MP","EBP.AEF.MAJOR_RETREAT_POINT_MP","EBP.AEF.MAJOR_UNLOCK_MP","EBP.AEF.MORTAR_TEAM_WEAPON_CREW_MP","EBP.AEF.OBSERVATION_POST_FUEL_AEF_MP","EBP.AEF.OBSERVATION_POST_MUNITION_AEF_MP","EBP.AEF.P47_RECON","EBP.AEF.P47_RECON_PLANE_SWEEP","EBP.AEF.P47_RECON_TRACKING","EBP.AEF.P47_ROCKETS","EBP.AEF.P47_STRAFE","EBP.AEF.PARATROOPER_MP","EBP.AEF.PARATROOPERS_COMBAT_GROUP_PLANE","EBP.AEF.PARATROOPERS_PLANE","EBP.AEF.PARATROOPERS_PLANE_ATGUN","EBP.AEF.PARATROOPERS_PLANE_HMG","EBP.AEF.PARATROOPERS_PLANE_MINES","EBP.AEF.PARATROOPERS_PLANE_PARAS","EBP.AEF.PATHFINDER_IR_MP","EBP.AEF.PATHFINDER_RECON_MP","EBP.AEF.PM_AEF_AIR_SUPPORT_RECON","EBP.AEF.PM_AEF_AIR_SUPPORT_ROCKET","EBP.AEF.PM_AEF_AIR_SUPPORT_ROCKET_ELITE","EBP.AEF.PM_AEF_AIR_SUPPORT_STRAFE","EBP.AEF.PM_AEF_AIR_SUPPORT_STRAFE_ELITE","EBP.AEF.PM_AEF_AIRBORNE_PARATROOPERS_PLANE_PARAS","EBP.AEF.PM_AEF_AIRBORNE_PARATROOPERS_PLANE_STRAFE","EBP.AEF.PM_AEF_AIRBORNE_PARATROOPERS_SPAWNER","EBP.AEF.PM_AEF_AIRBORNE_SUPPLY_DROP_PLANE","EBP.AEF.PM_AEF_FIGHTING_POSITION_TEAMWEAPONS","EBP.AEF.PM_AEF_PINPOINT_ARTY_MARKER_MP","EBP.AEF.PM_AEF_PINPOINT_ARTY_THREE_MARKER_MP","EBP.AEF.PM_ARMOR_COMMAND_BAZOOKA_RACK","EBP.AEF.PM_ARMOR_COMMAND_LMG_RACK","EBP.AEF.PM_ATTACHED_MEDIC","EBP.AEF.PM_ATTACHED_SEARGENT","EBP.AEF.PM_P47_FLYBY","EBP.AEF.PM_P47_MG_STRAFE","EBP.AEF.PM_P47_ROCKET_STRAFE","EBP.AEF.RANGER_COMMANDER_MP","EBP.AEF.RANGER_MP","EBP.AEF.REAR_ECHELON_RADIOMAN_MP","EBP.AEF.REAR_ECHELON_RESERVE_TROOP_MP","EBP.AEF.REAR_ECHELON_TROOP_CAPT_MP","EBP.AEF.REAR_ECHELON_TROOP_MP","EBP.AEF.REPLACEMENT_ARMOR_COMMAND_MP","EBP.AEF.REPLACEMENT_ARMORED_RIFLE_COMMAND_MP","EBP.AEF.REPLACEMENT_COMPANY_WEAPONS_POOL_MP","EBP.AEF.RIFLE_COMMAND_MP","EBP.AEF.RIFLE_COMMAND_SP","EBP.AEF.RIFLE_COMMAND_WRECK_MP","EBP.AEF.RIFLEMAN_SOLDIER_CAPTAIN_MP","EBP.AEF.RIFLEMAN_SOLDIER_GROUP_MP","EBP.AEF.RIFLEMAN_SOLDIER_LIEUTENANT_MP","EBP.AEF.RIFLEMAN_SOLDIER_MP","EBP.AEF.SHERMAN_BARRIER_DEFORM_MP","EBP.AEF.SHERMAN_BARRIER_DIRT_MP","EBP.AEF.SHERMAN_BARRIER_MUD_MP","EBP.AEF.SHERMAN_BARRIER_RUBBLE_MP","EBP.AEF.SHERMAN_BARRIER_SNOW_MP","EBP.AEF.T34_CALLIOPE_MP","EBP.AEF.TEMP_ACTIVE_STRUCTURE_SEARCHLIGHT","EBP.AEF.USF_MEDIC_MP","EBP.AEF.VEHICLE_CREW_BAZOOKA_MP","EBP.AEF.VEHICLE_CREW_TROOP_MP","EBP.AEF.VEHICLE_CREW_TROOP_REPAIR_STATION_MP","SBP.AEF.AEF_AIR_SUPPORT_RECON","SBP.AEF.AEF_AIR_SUPPORT_ROCKET","SBP.AEF.AEF_AIR_SUPPORT_ROCKET_ELITE","SBP.AEF.AEF_AIR_SUPPORT_STRAFE","SBP.AEF.AEF_AIR_SUPPORT_STRAFE_ELITE","SBP.AEF.AEF_ATTACK_PLANE_SQUAD","SBP.AEF.AEF_HALFTRACK_SQUAD_MP","SBP.AEF.ASSAULT_ENGINEER_SQUAD_5_MAN_MP","SBP.AEF.ASSAULT_ENGINEER_SQUAD_MP","SBP.AEF.CAPTAIN_SQUAD_MP","SBP.AEF.DODGE_WC51_50CAL_SQUAD_MP","SBP.AEF.DODGE_WC51_AMBULANCE_SQUAD_MP","SBP.AEF.DODGE_WC51_PATHFINDER_SQUAD_MP","SBP.AEF.DODGE_WC51_SQUAD_MP","SBP.AEF.JACKSON_SQUAD","SBP.AEF.LIEUTENANT_SQUAD_MP","SBP.AEF.M1_57MM_AT_GUN_SQUAD_BOB","SBP.AEF.M1_57MM_AT_GUN_SQUAD_MP","SBP.AEF.M1_75MM_PACK_HOWITZER_SQUAD_MP","SBP.AEF.M1_81MM_MORTAR_SQUAD_MP","SBP.AEF.M10_TANK_DESTROYER_SQUAD_MP","SBP.AEF.M15A1_AA_HALFTRACK_SQUAD_MP","SBP.AEF.M1919A4_HMG_SQUAD_MP","SBP.AEF.M2_60MM_MORTAR_CORE_SQUAD_MP","SBP.AEF.M2_60MM_MORTAR_SQUAD_MP","SBP.AEF.M2_60MM_MORTAR_SQUAD_MP_CLONE","SBP.AEF.M20_ASSAULT_ENGY_ANTITANK_SQUAD_MP","SBP.AEF.M20_UTILITY_CAR_SQUAD_MP","SBP.AEF.M21_MORTAR_HALFTRACK_SQUAD_MP","SBP.AEF.M26_PERSHING_MP","SBP.AEF.M2HB_50CAL_HMG_SQUAD_MP","SBP.AEF.M3_HALFTRACK_SQUAD_ASSAULT_MP","SBP.AEF.M3_HALFTRACK_SQUAD_MP","SBP.AEF.M36_TANK_DESTROYER_SQUAD_MP","SBP.AEF.M4A3_76MM_SHERMAN_BULLDOZER_SQUAD_MP","SBP.AEF.M4A3_76MM_SHERMAN_SQUAD_MP","SBP.AEF.M4A3_SHERMAN_SQUAD_DEMO_BURNOUT","SBP.AEF.M4A3_SHERMAN_SQUAD_MP","SBP.AEF.M4A3E8_SHERMAN_EASY_8_SQUAD_MP","SBP.AEF.M5A1_STUART_SQUAD_MP","SBP.AEF.M7B1_PRIEST_SQUAD_MP","SBP.AEF.M8_GREYHOUND_SQUAD_MP","SBP.AEF.M8A1_HMC_SQUAD_MP","SBP.AEF.MAJOR_SQUAD_MP","SBP.AEF.P47_FLYBY","SBP.AEF.P47_MG_STRAFE","SBP.AEF.P47_RECON","SBP.AEF.P47_RECON_PLANE_SWEEP","SBP.AEF.P47_RECON_TRACKING","SBP.AEF.P47_ROCKETS","SBP.AEF.P47_ROCKETS_STRAFE","SBP.AEF.P47_STRAFES","SBP.AEF.PARATROOPER_COMBAT_GROUP_SQUAD_MP","SBP.AEF.PARATROOPER_SQUAD_MP","SBP.AEF.PARATROOPER_SQUAD_SUPPORT_MP","SBP.AEF.PARATROOPERS_COMBAT_GROUP_PLANE","SBP.AEF.PARATROOPERS_PLANE","SBP.AEF.PARATROOPERS_PLANE_ATGUN","SBP.AEF.PARATROOPERS_PLANE_HMG","SBP.AEF.PARATROOPERS_PLANE_MINES","SBP.AEF.PARATROOPERS_PLANE_PARAS","SBP.AEF.PATHFINDER_SQUAD_MP","SBP.AEF.PATHFINDER_SQUAD_RECON_MP","SBP.AEF.PM_AEF_AIRBORNE_PARATROOPERS_PLANE_PARAS","SBP.AEF.PM_AEF_AIRBORNE_PARATROOPERS_PLANE_STRAFE","SBP.AEF.PM_AEF_AIRBORNE_SUPPLY_DROP_PLANE","SBP.AEF.PM_M3_HALFTRACK_SQUAD_OMCG","SBP.AEF.PM_RIFLEMEN_SQUAD_OMCG","SBP.AEF.RANGER_SQUAD_COMMANDER_MP","SBP.AEF.RANGER_SQUAD_MP","SBP.AEF.REAR_ECHELON_SQUAD_MP","SBP.AEF.RIFLEMEN_SQUAD_MP","SBP.AEF.RIFLEMEN_SQUAD_VETERAN_MP","SBP.AEF.T34_CALLIOPE_SQUAD_MP","SBP.AEF.USF_MEDIC_SQUAD_MP","SBP.AEF.VEHICLE_CREW_BAZOOKA_SQUAD_MP","SBP.AEF.VEHICLE_CREW_SQUAD_MP","ABILITY.AEF.ACTIVATE_REPAIR_STATION_MP","ABILITY.AEF.AEF_BARBED_WIRE_CUTTING_ABILITY_ASSUALT_ENGINEERS_MP","ABILITY.AEF.AEF_BARBED_WIRE_CUTTING_ABILITY_MP","ABILITY.AEF.AEF_BARBED_WIRE_CUTTING_ABILITY_NO_REQUIREMENT_MP","ABILITY.AEF.AEF_HQ_ENGINEER_CALL_IN","ABILITY.AEF.AEF_REPAIR_ABILITY_REAR_ECHELON_MP","ABILITY.AEF.AEF_REPAIR_ABILITY_VEHICLE_CREW_MP","ABILITY.AEF.AEF_REPAIR_CRITICAL_MP","ABILITY.AEF.AIR_DROP_COMBAT_GROUP","ABILITY.AEF.AMBULANCE_HEAL_AREA","ABILITY.AEF.ARTILLERY_155MM","ABILITY.AEF.ARTILLERY_SMOKE_BARRAGE","ABILITY.AEF.ASSAULT_ENGINEER_DISPATCH","ABILITY.AEF.BAR_SUPPRESSION_ABILITY","ABILITY.AEF.BAZOOKA_DEPLOY_MP","ABILITY.AEF.BEACON_DISABLE","ABILITY.AEF.CALLIOPE_ROCKET_BARRAGE_MP","ABILITY.AEF.CAPTAIN_SUPERVISE","ABILITY.AEF.CMD_PARATROOPERS_FROM_PATHFINDERS","ABILITY.AEF.COMBAT_ENGINEER_TIMED_DEMO_MP","ABILITY.AEF.COMBINED_ARMS","ABILITY.AEF.DODGE_WC51_DISPATCH","ABILITY.AEF.ELITE_RIFLEMEN","ABILITY.AEF.ELITE_VEHICLE_CREWS","ABILITY.AEF.FATALITY_P47_ROCKET_ATTACK","ABILITY.AEF.FATALITY_PARATROOPERS_PARADROP","ABILITY.AEF.FATALITY_SMOKE_FLARES","ABILITY.AEF.FATALITY_WHITE_PHOSPHOROUS_BARRAGE","ABILITY.AEF.FLANKING_SPEED","ABILITY.AEF.FORWARD_OBSERVERS_ALWAYS_ON","ABILITY.AEF.FORWARD_OBSERVERS_UNLOCK_2","ABILITY.AEF.GREYHOUND_RECON_DISPATCH","ABILITY.AEF.LIEUTENANT_CAPTAIN_ON_ME_AURA_MP","ABILITY.AEF.M1_81MM_MORTAR_TEAM_MORTAR_BARRAGE_MP","ABILITY.AEF.M1_81MM_MORTAR_TEAM_SMOKE_BARRAGE_MP","ABILITY.AEF.M1_81MM_MORTAR_WHITE_PHOSPHOROUS_BARRAGE_ABILITY_MP","ABILITY.AEF.M1_ATGUN_PIERCING_ABILITY","ABILITY.AEF.M1_ATGUN_TAKE_AIM_ABILITY","ABILITY.AEF.M10_APCPC_SHELLS","ABILITY.AEF.M10_APCPC_SHELLS_VET","ABILITY.AEF.M10_DEPLOY","ABILITY.AEF.M15A1_AA_MODE_MP","ABILITY.AEF.M2_60MM_MORTAR_TEAM_MORTAR_BARRAGE_MP","ABILITY.AEF.M2_60MM_MORTAR_TEAM_SMOKE_BARRAGE_MP","ABILITY.AEF.M20_MARK_VEHICLE","ABILITY.AEF.M20_SMOKE","ABILITY.AEF.M21_HEAVY_HE_SHORT_DELAY_MORTAR_BARRAGE_MP","ABILITY.AEF.M21_MORTAR_BARRAGE_MP","ABILITY.AEF.M21_MORTAR_BARRAGE_VICTOR_TARGET_MP","ABILITY.AEF.M21_MORTAR_HALFTRACK_DISPATCH","ABILITY.AEF.M21_MORTAR_WHITE_PHOSPHOROUS_BARRAGE_MP","ABILITY.AEF.M23_SMOKE_STREAM_RIFLE_GRENADE_MP","ABILITY.AEF.M23_SMOKE_STREAM_RIFLE_GRENADE_VET_MP","ABILITY.AEF.M26_PERSHING_DISPATCH","ABILITY.AEF.M2HB_50CAL_AP_ROUNDS_MP","ABILITY.AEF.M2HB_HMG_SPRINT_MP","ABILITY.AEF.M3_HALFTRACK_GROUP","ABILITY.AEF.M3_HALFTRACK_SPEED_BOOST_MP","ABILITY.AEF.M36_M8_CONCEALING_SMOKE_VET","ABILITY.AEF.M5_QUAD_HALFTRACK_DISPATCH","ABILITY.AEF.M5_STUART_DAMAGE_ENGINE","ABILITY.AEF.M5_STUART_SHELL_SHOCK","ABILITY.AEF.M7B1_PRIEST_105MM_BARRAGE_ABILITY_MP","ABILITY.AEF.M7B1_PRIEST_105MM_BARRAGE_ABILITY_VICTOR_TARGET_MP","ABILITY.AEF.M7B1_PRIEST_105MM_SMOKE_BARRAGE_ABILITY_MP","ABILITY.AEF.M8_CANISTER_SHOT","ABILITY.AEF.M8_LAY_HEAVY_MINE","ABILITY.AEF.M8A1_HMC_75MM_BARRAGE_ABILITY_MP","ABILITY.AEF.M8A1_HMC_75MM_BARRAGE_ABILITY_VICTOR_TARGET_MP","ABILITY.AEF.M8A1_HMC_SMOKE_BARRAGE_MP","ABILITY.AEF.MAJOR_ARTILLERY","ABILITY.AEF.MAJOR_ARTILLERY_FAKE","ABILITY.AEF.MAJOR_QUICK_RECON_RUN","ABILITY.AEF.MAJOR_QUICK_RECON_RUN_IMPROVED","ABILITY.AEF.MEDIC_AUTO_HEAL","ABILITY.AEF.MK2_FRAGMENTATION_GRENADE_MP","ABILITY.AEF.OFF_MAP_SMOKE_ARTILLERY","ABILITY.AEF.OFFICER_RETREAT_POINT_MP","ABILITY.AEF.OFFICER_STOP_RETREAT_MP","ABILITY.AEF.OUT_OF_FUEL_SP","ABILITY.AEF.P47_RECON_MP","ABILITY.AEF.P47_ROCKET_ATTACK","ABILITY.AEF.PACK_HOWITZER_75MM_BARRAGE_ABILITY_HEAT_MP","ABILITY.AEF.PACK_HOWITZER_75MM_BARRAGE_ABILITY_MP","ABILITY.AEF.PACK_HOWITZER_75MM_BARRAGE_ABILITY_VET3_MP","ABILITY.AEF.PACK_HOWITZER_75MM_BARRAGE_ABILITY_VICTOR_TARGET_MP","ABILITY.AEF.PACK_HOWITZER_WHITE_PHOSPHOROUS_BARRAGE_ABILITY_MP","ABILITY.AEF.PARADROP_MACHINE_GUN","ABILITY.AEF.PARADROPS_ANTI_TANK_GUN","ABILITY.AEF.PARATROOPER_ASSAULT_MOVE_TEST_MP","ABILITY.AEF.PARATROOPER_MK2_FRAGMENTATION_GRENADE_MP","ABILITY.AEF.PARATROOPER_SUPPRESSING_FIRE_ABILITY_MP","ABILITY.AEF.PARATROOPER_TIMED_DEMO_MP","ABILITY.AEF.PARATROOPERS_PARADROP","ABILITY.AEF.PATHFINDER_ARTILLERY_UNLOCK","ABILITY.AEF.PATHFINDER_IN_COVER_STATIONARY_CAMOUFLAGE_IMPROVED_MP","ABILITY.AEF.PATHFINDER_IN_COVER_STATIONARY_CAMOUFLAGE_MP","ABILITY.AEF.PATHFINDER_PLANT_BEACON","ABILITY.AEF.PATHFINDERS_DISPATCH","ABILITY.AEF.PATHFINDERS_RECON_DISPATCH","ABILITY.AEF.PERSHING_HVAP_PIERCING_SHOT_ABILITY","ABILITY.AEF.PRIEST_ARTILLERY_BARRAGE_CREEPING_MP","ABILITY.AEF.PRIEST_DISPATCH","ABILITY.AEF.RANGER_BUNDLED_GRENADE_MP","ABILITY.AEF.RANGER_LIMITED_DEMO_MP","ABILITY.AEF.RANGER_MK2_FRAGMENTATION_GRENADE_MP","ABILITY.AEF.RANGER_SPRINT_MP","ABILITY.AEF.RANGERS_DISPATCH","ABILITY.AEF.REAR_ECHELON_VOLLEY_FIRE_ABILITY_MP","ABILITY.AEF.RECON_SWEEP","ABILITY.AEF.REFUEL_TANK_SP","ABILITY.AEF.RIFLEMAN_AT_RIFLE_GRENADE_VET","ABILITY.AEF.RIFLEMAN_FIRE_UP","ABILITY.AEF.RIFLEMAN_FIRE_UP_MP","ABILITY.AEF.RIFLEMEN_30_CALIBER_LMG","ABILITY.AEF.RIFLEMEN_DEFENSIVE","ABILITY.AEF.RIFLEMEN_DEFENSIVE_BUILDINGS","ABILITY.AEF.RIFLEMEN_FIRE_FLARES_ABILITY_MP","ABILITY.AEF.RIFLEMEN_FLAMETHROWERS","ABILITY.AEF.RIFLEMEN_FLARES","ABILITY.AEF.SHERMAN_AMMO_SWITCH_AP_SHELL_MP","ABILITY.AEF.SHERMAN_AMMO_SWITCH_HE_SHELL_MP","ABILITY.AEF.SHERMAN_BULLDOZER_CONSTRUCT_BARRIER_MP","ABILITY.AEF.SHERMAN_BULLDOZER_DESTROY_BARRIER_MP","ABILITY.AEF.SHERMAN_BULLDOZER_DISPATCH","ABILITY.AEF.SHERMAN_CALLIOPE_DISPATCH","ABILITY.AEF.SHERMAN_EASY8_DISPATCH","ABILITY.AEF.SIEGE_240MM_BARRAGE","ABILITY.AEF.SMOKE_SHERMAN_MORTAR_BARRAGE_BULLDOZER_MP","ABILITY.AEF.SMOKE_SHERMAN_MORTAR_BARRAGE_MP","ABILITY.AEF.SP_240MM_OFF_MAP_BARRAGE","ABILITY.AEF.SP_QUICK_RECON_RUN","ABILITY.AEF.SUPPORT_ARTILLERY","ABILITY.AEF.SUPPORT_ARTILLERY_DECOY","ABILITY.AEF.TANK_RIDERS_AUTO_UNLOAD_MP","ABILITY.AEF.TIME_ON_TARGET_ARTILLERY","ABILITY.AEF.USF_HOLD_FIRE_MP","ABILITY.AEF.USF_HOLD_FIRE_PACK_HOWITZER_MP","ABILITY.AEF.USF_MEDIC_HEAL_MP","ABILITY.AEF.USF_SHERMAN_BULLDOZER_HOLD_FIRE_MP","ABILITY.AEF.USF_STRAFING_RUN","ABILITY.AEF.USF_VEHICLE_HOLD_FIRE_MP","ABILITY.AEF.VEHICLE_CREW_AUTO_REPAIR","ABILITY.AEF.VEHICLE_DECREW_GENERIC_MP","ABILITY.AEF.VEHICLE_DECREW_M20_CREW_MP","ABILITY.AEF.VEHICLE_DECREW_MEDICS_MP","ABILITY.AEF.VEHICLE_DECREW_VEHICLE_CREW_MP","ABILITY.AEF.WC51_SPEED_BOOST_MP","ABILITY.AEF.WITHDRAW_AND_REFIT","UPG.AEF.ABILITY_LOCK_OUT_CAPTAIN_ABILITIES","UPG.AEF.ABILITY_LOCK_OUT_LIEUTENANT_ABILITIES","UPG.AEF.ABILITY_LOCK_OUT_PARATROOPERS_LANDED","UPG.AEF.ABILITY_REFUEL_LOCKOUT","UPG.AEF.ABILITY_TRANSFER_ORDERS_LOCK_OUT","UPG.AEF.ARTILLERY_155MM","UPG.AEF.ARTILLERY_155MM_BLIND","UPG.AEF.ARTILLERY_WHITE_PHOSPHOROUS","UPG.AEF.ASSAULT_ENGINEER_DISPATCH","UPG.AEF.ASSAULT_ENGINEER_FLAMETHROWER","UPG.AEF.BAR_UPGRADE_MP","UPG.AEF.BAZOOKA_UPGRADE_MP","UPG.AEF.CAPTAIN_BAZOOKA_UPGRADE_MP","UPG.AEF.CAPTAIN_DISPATCHED_UPGRADE_MP","UPG.AEF.COMBINED_ARMS_MP","UPG.AEF.DODGE_WC51_DISPATCH","UPG.AEF.ELITE_RIFLEMEN","UPG.AEF.ELITE_VEHICLE_CREWS","UPG.AEF.FIGHTING_POSITION_MG_ADDITION_MP","UPG.AEF.FIRE_UP_RIFLEMEN","UPG.AEF.FORWARD_OBSERVERS_UNLOCK","UPG.AEF.GREYHOUND_RECON_DISPATCH","UPG.AEF.LIEUTENANT_DISPATCHED_UPGRADE_MP","UPG.AEF.M10_DEPLOY","UPG.AEF.M20_SIDE_SKIRTS_MP","UPG.AEF.M21_MORTAR_HALFTRACK_DISPATCH","UPG.AEF.M26_PERSHING_DISPATCH","UPG.AEF.M3_HALFTRACK_GROUP","UPG.AEF.M3_REPAIR_STATION_MP","UPG.AEF.M5_HALFTRACK_DISPATCH","UPG.AEF.M8_GREYHOUND_SIDE_SKIRTS_MP","UPG.AEF.M8_TOP_GUNNER_MP","UPG.AEF.MAJOR_DISPATCHED_UPGRADE_MP","UPG.AEF.MEDIC_AUTO_HEAL_REFRESH","UPG.AEF.MINESWEEPER_UPGRADE_MP","UPG.AEF.NO_OFFICER_SPAWN_MP","UPG.AEF.OFF_SMOKE_BARRAGE","UPG.AEF.P47_RECON","UPG.AEF.P47_ROCKET_ATTACK","UPG.AEF.PARADROP_ANTI_TANK_GUN","UPG.AEF.PARADROP_MACHINE_GUN","UPG.AEF.PARADROPPED_SUPPORT_DROP","UPG.AEF.PARATROOPER_M1919A6_LMG_MP","UPG.AEF.PARATROOPER_THOMPSON_SUB_MACHINE_GUN_UPGRADE_MP","UPG.AEF.PARATROOPERS","UPG.AEF.PATHFINDERS","UPG.AEF.PATHFINDERS_RECON","UPG.AEF.PRIEST_DISPATCH","UPG.AEF.RANGER_DISPATCH","UPG.AEF.RANGER_THOMPSON_SUB_MACHINE_GUN_UPGRADE_MP","UPG.AEF.REAR_ECHELON_HACK_WITHDRAWING","UPG.AEF.RECON_SWEEP","UPG.AEF.RIFLE_COMMAND_GRENADE_MP","UPG.AEF.RIFLEMEN_30_CALIBER_LMG","UPG.AEF.RIFLEMEN_DEFENSIVE_BUILDINGS","UPG.AEF.RIFLEMEN_FLAMETHROWER","UPG.AEF.RIFLEMEN_FLAMETHROWER_UNLOCK","UPG.AEF.RIFLEMEN_FLARES","UPG.AEF.SHERMAN_BULLDOZER_DISPATCH","UPG.AEF.SHERMAN_EASY8_DISPATCH","UPG.AEF.SHERMAN_HE_ROUNDS","UPG.AEF.SHERMAN_TOP_GUNNER_MP","UPG.AEF.SIEGE_240MM_ARTILLERY","UPG.AEF.SMOKE_BARRAGE","UPG.AEF.T34_SHERMAN_CALLIOPE_DISPATCH","UPG.AEF.TECH_TREE_V1","UPG.AEF.TEMP_SPAWN_BASE_STAMP_MP","UPG.AEF.TIME_ON_TARGET_ARTILLERY","UPG.AEF.TOP_GUNNER_UPGRADED","UPG.AEF.USF_M5_HALFTRACK_72K_AA_GUN_PACKAGE_MP","UPG.AEF.USF_STRAFING_RUN","UPG.AEF.VEHICLE_CREW_THOMPSON_SUB_MACHINE_GUN_UPGRADE_MP","UPG.AEF.WEAPON_RACK_UPGRADE_MP","UPG.AEF.WITHDRAW_AND_REFIT","EBP.BRITISH.AEC_ARMOURED_CAR_MP","EBP.BRITISH.AIR_SUPPORT_OFFICER_MP","EBP.BRITISH.AVRE_VEHICLE_CREW_MP","EBP.BRITISH.BRIT_17_POUNDER_GUN_MP","EBP.BRITISH.BRIT_17_POUNDER_PIT_COMMANDER_MP","EBP.BRITISH.BRIT_17_POUNDER_PIT_MP","EBP.BRITISH.BRIT_25_POUNDER_HOWITZER_MP","EBP.BRITISH.BRIT_25_POUNDER_HOWITZER_TEMP_MP","EBP.BRITISH.BRIT_3_INCH_MORTAR_EMPLACEMENT","EBP.BRITISH.BRIT_3_INCH_MORTAR_EMPLACEMENT_COMMANDER_MP","EBP.BRITISH.BRIT_6_POUNDER_AT_GUN_MP","EBP.BRITISH.BRIT_BARBED_WIRE_FENCE_MP","EBP.BRITISH.BRIT_BOFORS_40MM_AUTOCANNON_COMMANDER_MP","EBP.BRITISH.BRIT_BOFORS_40MM_AUTOCANNON_MP","EBP.BRITISH.BRIT_EMPLACEMENT_SMALL","EBP.BRITISH.BRIT_FORWARD_HQ_COMMANDER_MP","EBP.BRITISH.BRIT_FORWARD_HQ_MP","EBP.BRITISH.BRIT_FWD_HQ_WEAPON_RACK_BREN_LMG_MP","EBP.BRITISH.BRIT_FWD_HQ_WEAPON_RACK_PIAT_LAUNCHER_MP","EBP.BRITISH.BRIT_LAND_MATTRESS_LAUNCHER_MP","EBP.BRITISH.BRIT_MEDIC_EXTRA_ENTITY_MP","EBP.BRITISH.BRIT_MEDIC_WITH_PISTOL_MP","EBP.BRITISH.BRIT_MINE_COMMANDER_MP","EBP.BRITISH.BRIT_MINE_MP","EBP.BRITISH.BRIT_RETREAT_POINT_MP","EBP.BRITISH.BRIT_SANDBAG_FENCE","EBP.BRITISH.BRIT_WEAPON_RACK_BREN_LMG_MP","EBP.BRITISH.BRIT_WEAPON_RACK_PIAT_LAUNCHER_MP","EBP.BRITISH.BRITISH_25LB_HOWITZER_GUN_CREW_MP","EBP.BRITISH.BRITISH_6LB_AT_GUN_CREW_MP","EBP.BRITISH.BRITISH_BASE_STAMPER","EBP.BRITISH.BRITISH_BUILDING_1_MP","EBP.BRITISH.BRITISH_BUILDING_1_UNBUILT_MP","EBP.BRITISH.BRITISH_BUILDING_1_WRECK_MP","EBP.BRITISH.BRITISH_BUILDING_2_MP","EBP.BRITISH.BRITISH_BUILDING_2_UNBUILT_MP","EBP.BRITISH.BRITISH_BUILDING_2_WRECK_MP","EBP.BRITISH.BRITISH_BUNKER_STARTING_POSITION_MP","EBP.BRITISH.BRITISH_HMG_PLANE","EBP.BRITISH.BRITISH_HMG_TEAM_CREW_MP","EBP.BRITISH.BRITISH_HQ_SANDBAGS_01_MP","EBP.BRITISH.BRITISH_HQ_TRUCK_MP","EBP.BRITISH.BRITISH_HQ_TRUCK_WRECK_MP","EBP.BRITISH.BRITISH_LAND_MATTRESS_TEAM_CREW_MP","EBP.BRITISH.BRITISH_MACHINE_GUN_MP","EBP.BRITISH.BRITISH_MORTAR_TEAM_CREW_MP","EBP.BRITISH.BRITISH_RADIO_BEACON","EBP.BRITISH.BRITISH_SANDBAG_FENCE_MP","EBP.BRITISH.CENTAUR_AA_MK2_MP","EBP.BRITISH.CHURCHILL_AVRE_MP","EBP.BRITISH.CHURCHILL_CROCODILE_MP","EBP.BRITISH.CHURCHILL_DEFAULT_MP","EBP.BRITISH.COMET_MP","EBP.BRITISH.COMMANDO_AIR_LANDING_MP","EBP.BRITISH.COMMANDO_MP","EBP.BRITISH.COMMANDO_PIAT_MP","EBP.BRITISH.CROMWELL_MK4_75MM_MP","EBP.BRITISH.FIELD_HOSPITAL_MEDIC_MP","EBP.BRITISH.FORWARD_OBSERVATION_OFFICER_MP","EBP.BRITISH.GLIDER_COMMANDOS_ONLY","EBP.BRITISH.GLIDER_HEADQUARTERS","EBP.BRITISH.HQ_FIELD_ARTILLERY_MP","EBP.BRITISH.INVISIBLE_FLAME_MORTAR_ICON","EBP.BRITISH.M3_HALFTRACK_RESUPPLY_MP","EBP.BRITISH.PARATROOPERS_PLANE_ATGUN_MATT_TEST_MP","EBP.BRITISH.PARATROOPERS_PLANE_VICKERS_MATT_TEST_MP","EBP.BRITISH.RECON_HAWKER_TYPHOON_ASSAULT_MP","EBP.BRITISH.RECON_HAWKER_TYPHOON_MP","EBP.BRITISH.REPAIR_SAPPER_MP","EBP.BRITISH.ROCKET_HAWKER_TYPHOON_MP","EBP.BRITISH.SAPPER_MP","EBP.BRITISH.SAPPER_RECOVERY_MP","EBP.BRITISH.SEXTON_SPG_MP","EBP.BRITISH.SHERMAN_FIREFLY_M4A2_MP","EBP.BRITISH.SLIT_TRENCH_MP","EBP.BRITISH.SNIPER_BRITISH_MP","EBP.BRITISH.SPITFIRE_RECON_PLANE","EBP.BRITISH.STRAFE_HAWKER_TYPHOON_MP","EBP.BRITISH.TOMMY_MP","EBP.BRITISH.TOMMY_RECON_MP","EBP.BRITISH.UNIVERSAL_CARRIER_MP","EBP.BRITISH.UNIVERSAL_CARRIER_RESUPPLY_MP","EBP.BRITISH.VALENTINE_MORTAR","EBP.BRITISH.VALENTINE_OBSERVATION_MP","EBP.BRITISH.VEHICLE_CREW_MP","SBP.BRITISH.AEC_ARMOURED_CAR_SQUAD_MP","SBP.BRITISH.AIR_SUPPORT_OFFICER_SQUAD_MP","SBP.BRITISH.AVRE_VEHICLE_CREW_SQUAD_MP","SBP.BRITISH.BRIT_17_POUNDER_AT_GUN_SQUAD_COMMANDER_MP","SBP.BRITISH.BRIT_17_POUNDER_AT_GUN_SQUAD_MP","SBP.BRITISH.BRIT_25_POUNDER_HOWITZER_SQUAD_MP","SBP.BRITISH.BRIT_25_POUNDER_HOWITZER_SQUAD_TEMP_MP","SBP.BRITISH.BRIT_3_INCH_MORTAR_TEAM_COMMANDER_MP","SBP.BRITISH.BRIT_3_INCH_MORTAR_TEAM_MP","SBP.BRITISH.BRIT_6_POUNDER_AT_GUN_SQUAD_MP","SBP.BRITISH.BRIT_BOFORS_40MM_AUTOCANNON_SQUAD_COMMANDER_MP","SBP.BRITISH.BRIT_BOFORS_40MM_AUTOCANNON_SQUAD_MP","SBP.BRITISH.BRIT_BREN_LMG_WEAPON_RACK_UI_FAKE_MP","SBP.BRITISH.BRIT_FORWARD_HQ_MP","SBP.BRITISH.BRIT_LAND_MATTRESS_LAUNCHER_SQUAD_MP","SBP.BRITISH.BRIT_MEDIC_SQUAD_MP","SBP.BRITISH.BRIT_PIAT_LAUNCHER_WEAPON_RACK_UI_FAKE_MP","SBP.BRITISH.BRITISH_CARGO_PLANE","SBP.BRITISH.BRITISH_HMG_PLANE","SBP.BRITISH.BRITISH_MACHINE_GUN_SQUAD_MP","SBP.BRITISH.CENTAUR_AA_MK2_SQUAD_MP","SBP.BRITISH.CHURCHILL_AVRE_SQUAD_MP","SBP.BRITISH.CHURCHILL_CROCODILE_MP","SBP.BRITISH.CHURCHILL_DEFAULT_SQUAD_MP","SBP.BRITISH.COMET_TANK_SQUAD_MP","SBP.BRITISH.COMMANDO_SQUAD_MP","SBP.BRITISH.COMMANDO_SQUAD_PIAT_MP","SBP.BRITISH.CROMWELL_MK4_75MM_SQUAD_MP","SBP.BRITISH.FORWARD_HQ_MP","SBP.BRITISH.FORWARD_OBSERVATION_SQUAD_MP","SBP.BRITISH.GLIDER_COMMANDOS_ONLY_MP","SBP.BRITISH.GLIDER_HEADQUARTERS_MP","SBP.BRITISH.INFILTRATION_COMMANDO_SQUAD_MP","SBP.BRITISH.M3_HALFTRACK_SQUAD__RESUPPLY_MP","SBP.BRITISH.PARATROOPERS_PLANE_ATGUN_BRITISH_MATT_TEST_MP","SBP.BRITISH.PARATROOPERS_PLANE_VICKERS_BRITISH_MATT_TEST_MP","SBP.BRITISH.RECON_HAWKER_TYPHOON_ASSAULT_MP","SBP.BRITISH.RECON_HAWKER_TYPHOON_MP","SBP.BRITISH.ROCKET_HAWKER_TYPHOON_MP","SBP.BRITISH.SAPPER_SQUAD_DEMOLITION_MP","SBP.BRITISH.SAPPER_SQUAD_MP","SBP.BRITISH.SAPPER_SQUAD_RECOVERY_MP","SBP.BRITISH.SEXTON_SPG_SQUAD_MP","SBP.BRITISH.SHERMAN_FIREFLY_SQUAD_MP","SBP.BRITISH.SNIPER_BRITISH_SQUAD_MP","SBP.BRITISH.SPITFIRE_RECON_PLANE","SBP.BRITISH.STRAFE_HAWKER_TYPHOON_MP","SBP.BRITISH.TOMMY_SQUAD_FLAME_MP","SBP.BRITISH.TOMMY_SQUAD_MP","SBP.BRITISH.TOMMY_SQUAD_RECON_MP","SBP.BRITISH.TOMMY_SQUAD_TANK_HUNTER_MP","SBP.BRITISH.UNIVERSAL_CARRIER_RESUPPLY","SBP.BRITISH.UNIVERSAL_CARRIER_SQUAD_MP","SBP.BRITISH.VALENTINE_MORTAR","SBP.BRITISH.VALENTINE_OBSERVATION_MP","SBP.BRITISH.VEHICLE_CREW_STANDARD_SQUAD_MP","ABILITY.BRITISH.ADVANCED_ASSEMBLY","ABILITY.BRITISH.ADVANCED_COVER_COMBAT","ABILITY.BRITISH.AEC_DEFENSIVE_SMOKE","ABILITY.BRITISH.AEC_TREAD_SHOTS_MP","ABILITY.BRITISH.ALLIED_STRATEGIC_BOMBING","ABILITY.BRITISH.ARTILLERY_COVER","ABILITY.BRITISH.ASSAULT","ABILITY.BRITISH.ASSAULT_GRENADES","ABILITY.BRITISH.AT_GUN_AIRDROP","ABILITY.BRITISH.AVRE_CREW_DEMOLITION_CHARGE_MP","ABILITY.BRITISH.AVRE_CREW_SHRAPNELL_GRENADE_MP","ABILITY.BRITISH.AVRE_SPIGOT_MORTAR_ATTACK_MP","ABILITY.BRITISH.AVRE_SPIGOT_MORTAR_ATTACK_VET_3_MP","ABILITY.BRITISH.AVRE_SPIGOT_MORTAR_RELOAD_MP","ABILITY.BRITISH.AVRE_VEHICLE_DECREW_VEHICLE_CREW_MP","ABILITY.BRITISH.BOFORS_SUPPRESSIVE_BARRAGE_ABILITY_MP","ABILITY.BRITISH.BOFORS_SUPPRESSIVE_BARRAGE_ABILITY_VICTOR_TARGET_MP","ABILITY.BRITISH.BREAKTHROUGH_OPERATION","ABILITY.BRITISH.BRIT_17_POUNDER_FACING_ORDER_MP","ABILITY.BRITISH.BRIT_17_POUNDER_FLARES_ABILITY_MP","ABILITY.BRITISH.BRIT_17_POUNDER_PIERCING_SHELL_ABILITY_MP","ABILITY.BRITISH.BRIT_17_POUNDER_PIERCING_SHELL_ABILITY_VICTOR_TARGET_MP","ABILITY.BRITISH.BRIT_3_INCH_MORTAR_EMPLACEMENT_BARRAGE_MP","ABILITY.BRITISH.BRIT_3_INCH_MORTAR_EMPLACEMENT_BARRAGE_VICTOR_TARGET_MP","ABILITY.BRITISH.BRIT_3_INCH_MORTAR_EMPLACEMENT_SMOKE_BARRAGE_MP","ABILITY.BRITISH.BRIT_6_POUNDER_CRITICAL_SHOT_MP","ABILITY.BRITISH.BRIT_6_POUNDER_RAPID_MANEUVER_MP","ABILITY.BRITISH.BRIT_BARBED_WIRE_CUTTING_ABILITY_MP","ABILITY.BRITISH.BRIT_BASE_BRACED_STATIC_MP","ABILITY.BRITISH.BRIT_BASE_BUILDING_BRACED_OFF_MP","ABILITY.BRITISH.BRIT_BASE_BUILDING_BRACED_ON_MP","ABILITY.BRITISH.BRIT_EMPLACEMENT_BRACED_MP","ABILITY.BRITISH.BRIT_HQ_ENGINEER_CALL_IN","ABILITY.BRITISH.BRIT_MEDIC_HEAL_MP","ABILITY.BRITISH.BRIT_MEDIC_SQUAD_AUTO_HEAL","ABILITY.BRITISH.BRIT_MEDIC_TOMMY_TIMED_AREA_HEAL_MP","ABILITY.BRITISH.BRIT_MORTAR_EMPLACEMENT_HOLD_FIRE","ABILITY.BRITISH.BRIT_RADAR_SWEEP","ABILITY.BRITISH.BRIT_REPAIR_ABILITY_SAPPERS_MP","ABILITY.BRITISH.BRIT_REPAIR_ABILITY_TOMMYS_MP","ABILITY.BRITISH.BRIT_REPAIR_EWS_ABILITY_SAPPERS_MP","ABILITY.BRITISH.BRIT_SNIPER_DELAYED_COVER_AUTO_CAMOUFLAGE_MP","ABILITY.BRITISH.BRIT_TUNE_UP","ABILITY.BRITISH.BRIT_VEHICLE_HOLD_FIRE_MP","ABILITY.BRITISH.BRITISH_HOLD_THE_LINE","ABILITY.BRITISH.BRITISH_MORTAR_HOLD_FIRE_MP","ABILITY.BRITISH.CENTAUR_20MM_BARRAGE_MP","ABILITY.BRITISH.CENTAUR_AA_MODE_MP","ABILITY.BRITISH.CENTAUR_WEAPON_BURST_MP","ABILITY.BRITISH.CENTAUR_WEAPON_BURST_TEST_MP","ABILITY.BRITISH.CHURCHILL_AVRE","ABILITY.BRITISH.CHURCHILL_CREW_GRENADE_TARGETED","ABILITY.BRITISH.CHURCHILL_CROC_FLAME_BURST_MP","ABILITY.BRITISH.CHURCHILL_CROCODILE","ABILITY.BRITISH.CHURCHILL_INF_SUPPORT_SMOKE","ABILITY.BRITISH.COMET_CREW_GRENADE_TARGETED","ABILITY.BRITISH.COMET_SMOKE_SHELL_SHOT_MP","ABILITY.BRITISH.COMET_SMOKE_SHELL_SHOT_WP_MP","ABILITY.BRITISH.COMMAND_HQ","ABILITY.BRITISH.COMMAND_HQ_HE_ARTILLERY","ABILITY.BRITISH.COMMAND_HQ_RECON_PLANE","ABILITY.BRITISH.COMMAND_HQ_SMOKE_ARTILLERY","ABILITY.BRITISH.COMMAND_HQ_STRAFE_PLANE","ABILITY.BRITISH.COMMAND_VEHICLE","ABILITY.BRITISH.COMMAND_VEHICLE_PLANE","ABILITY.BRITISH.COMMANDO_ASSASSINATE_MP","ABILITY.BRITISH.COMMANDO_DEMO_MP","ABILITY.BRITISH.COMMANDO_INFILTRATION_CAMOUFLAGE_MP","ABILITY.BRITISH.COUNTER_BATTERY","ABILITY.BRITISH.COUNTER_BATTERYS","ABILITY.BRITISH.COVER_SMOKE_GRENADES","ABILITY.BRITISH.CREW_REPAIR","ABILITY.BRITISH.CREW_REPAIR_OPERATION","ABILITY.BRITISH.CROMWELL_SMOKE_SHELL_SHOT_MP","ABILITY.BRITISH.DEFENSIVE_OPERATIONS","ABILITY.BRITISH.DESTROY_COVER_MP","ABILITY.BRITISH.DIRECT_BARRAGE","ABILITY.BRITISH.EARLY_WARNING","ABILITY.BRITISH.ENGINEER_COVER_COMBAT_BONUS","ABILITY.BRITISH.FATALITY_BURN_THEM_OUT","ABILITY.BRITISH.FATALITY_MIGHT_OF_THE_AIR_FORCES","ABILITY.BRITISH.FATALITY_ZEROING_STRIKE","ABILITY.BRITISH.FIELD_RECOVERY","ABILITY.BRITISH.FIRE_SUPPORT_TEAM","ABILITY.BRITISH.FIREFLY_TULIP_ROCKET_BARRAGE_MP","ABILITY.BRITISH.FIREFLY_TULIP_ROCKET_BARRAGE_SKILL_SHOT_MP","ABILITY.BRITISH.FORTIFY_OUR_POSITION","ABILITY.BRITISH.FORWARD_HQ_RETREAT_POINT_GLIDER_MP","ABILITY.BRITISH.FORWARD_HQ_RETREAT_POINT_MP","ABILITY.BRITISH.GLIDER_COMMANDOS_ONLY","ABILITY.BRITISH.GLIDER_HEADQUARTERS","ABILITY.BRITISH.GLIDER_RETREAT_POINT_MP","ABILITY.BRITISH.HOWITZER_COUNTER_BARRAGE_ATTACK_COMMANDER_MP","ABILITY.BRITISH.HOWITZER_COUNTER_BARRAGE_COMMANDER_MP","ABILITY.BRITISH.HQ_BUILD_ANVIL_1_MP","ABILITY.BRITISH.HQ_BUILD_ANVIL_2_MP","ABILITY.BRITISH.IMPROVED_FORTIFCATIONS","ABILITY.BRITISH.INFANTRY_RECON_TACTICS","ABILITY.BRITISH.INFANTRY_SMOKE_GRENADE_RESPOSITION","ABILITY.BRITISH.INFILTRATION_COMMANDOS","ABILITY.BRITISH.LAND_MATTRESS","ABILITY.BRITISH.LAND_MATTRESS_25LB_ROCKET","ABILITY.BRITISH.LAND_MATTRESS_60LB_ROCKET","ABILITY.BRITISH.LAND_MATTRESS_BARRAGE","ABILITY.BRITISH.LAND_MATTRESS_BARRAGE_SMOKE","ABILITY.BRITISH.LAND_MATTRESS_BARRAGE_VICTOR_TARGET_MP","ABILITY.BRITISH.LAND_MATTRESS_FIRE_ALL","ABILITY.BRITISH.LAND_MATTRESS_LOAD_ROCKETS_MP","ABILITY.BRITISH.LAND_MATTRESS_PHOSPHORUS_ROCKET","ABILITY.BRITISH.MEDIC_AUTO_HEAL_MP","ABILITY.BRITISH.MORTAR_ARTILLERY","ABILITY.BRITISH.MORTAR_FIRE_ARTILLERY","ABILITY.BRITISH.MORTAR_PIT_COUNTER_BATTERY_MP","ABILITY.BRITISH.OBSERVATION_MODE","ABILITY.BRITISH.OBSERVATION_VALENTINE","ABILITY.BRITISH.OFFICER_ARTILLERY","ABILITY.BRITISH.OFFICER_ARTILLERY_SEXTON_VICTOR_TARGET_AIRBURST_BARRAGE_MP","ABILITY.BRITISH.OFFICER_ARTILLERY_SEXTON_VICTOR_TARGET_CONCENTRATION_BARRAGE_MP","ABILITY.BRITISH.OFFICER_CHARGE_MP","ABILITY.BRITISH.OFFICER_RECON_SWEEP","ABILITY.BRITISH.PASSIVE_17_POUNDER_EMPLACEMENT_MP","ABILITY.BRITISH.PASSIVE_BOFORS_EMPLACEMENT_MP","ABILITY.BRITISH.PASSIVE_MORTAR_EMPLACEMENT_MP","ABILITY.BRITISH.PEPPER_POT","ABILITY.BRITISH.PERCISION_BARRAGE","ABILITY.BRITISH.PIAT_DEPLOY_MP","ABILITY.BRITISH.QF_25_PDR_FLARE_BARRAGE_ABILITY_MP","ABILITY.BRITISH.QF_25LB_ANTITANK_ABILITY_BASE_MP","ABILITY.BRITISH.QF_25LB_ANTITANK_ABILITY_MP","ABILITY.BRITISH.QF_25LB_BARRAGE_ABILITY_BASE_MP","ABILITY.BRITISH.QF_25LB_BARRAGE_ABILITY_MP","ABILITY.BRITISH.QF_25LB_COORDINATED_FIRE_BASE_MP","ABILITY.BRITISH.QF_25LB_COORDINATED_FIRE_MP","ABILITY.BRITISH.QF_25LB_COORDINATED_FIRE_ORDER_BASE_MP","ABILITY.BRITISH.QF_25LB_COORDINATED_FIRE_ORDER_FWD_HQ_MP","ABILITY.BRITISH.QF_25LB_COORDINATED_FIRE_ORDER_MP","ABILITY.BRITISH.QF_25LB_COORDINATED_FIRE_ORDER_OFFICER_MP","ABILITY.BRITISH.QF_25LB_COORDINATED_FIRE_ORDER_SNIPER_MP","ABILITY.BRITISH.QF_25LB_COORDINATED_FIRE_ORDER_VALENTINE_MP","ABILITY.BRITISH.QF_25LB_COORDINATED_SMOKE_SCREEN_BASE_MATT_TEST_VICTOR_TARGET_MP","ABILITY.BRITISH.QF_25LB_CREEPING_SMOKE_BARRAGE_ABILITY_BASE_MP","ABILITY.BRITISH.QF_25LB_CREEPING_SMOKE_BARRAGE_ABILITY_MP","ABILITY.BRITISH.QF_25LB_DIRECT_BARRAGE_BASE_MP","ABILITY.BRITISH.QF_25LB_OVERWATCH_BASE_MP","ABILITY.BRITISH.QF_25LB_RAPID_RESPONSE_BARRAGE_BASE_MP","ABILITY.BRITISH.QF_25LB_RAPID_RESPONSE_BARRAGE_MP","ABILITY.BRITISH.QF_25LB_SMOKE_SCREEN_BASE_MP","ABILITY.BRITISH.RAPID_ADVANCE","ABILITY.BRITISH.RAPID_RESPONSE_ARTILLERY","ABILITY.BRITISH.RECON_SECTION_SPRINT_MP","ABILITY.BRITISH.REINFORCE_THE_FRONT","ABILITY.BRITISH.REINFORCED_STRUCTURES","ABILITY.BRITISH.SAPPER_ANVIL_BOOBY_TRAP","ABILITY.BRITISH.SAPPER_FLAMETHROWERS","ABILITY.BRITISH.SAPPER_GAMMON_BOMB_MEDIUM_MP","ABILITY.BRITISH.SAPPER_SALVAGE_WRECK","ABILITY.BRITISH.SEXTON_ARTILLERY_BARRAGE_CREEPING_VICTOR_TARGET_MP","ABILITY.BRITISH.SEXTON_DISPATCH_BRITISH","ABILITY.BRITISH.SEXTON_SPG_25_CONCENTRATION_BARRAGE_MP","ABILITY.BRITISH.SEXTON_SPG_25_PDR_ARTILLERY_CREEPING_BARRAGE_MP","ABILITY.BRITISH.SEXTON_SPG_25_PDR_BARRAGE_ABILITY_MP","ABILITY.BRITISH.SEXTON_SPG_25_PDR_SUPERCHARGE_AIRBURST_BARRAGE_ABILITY_MP","ABILITY.BRITISH.SEXTON_SPG_25_PDR_SUPERCHARGE_BARRAGE_ABILITY_MP","ABILITY.BRITISH.SMOKE_ASSAULT","ABILITY.BRITISH.SNIPER_BOYS_ANTI_TANK_CRITICAL_SHOT_MP","ABILITY.BRITISH.STAND_FAST","ABILITY.BRITISH.STRAFING_RUN","ABILITY.BRITISH.SUPER_OVERWATCH_TEST","ABILITY.BRITISH.TANK_HUNTER","ABILITY.BRITISH.TOMMY_COVER_COMBAT_BONUS","ABILITY.BRITISH.TOMMY_GAMMON_BOMB_HEAVY_MP","ABILITY.BRITISH.TOMMY_GAMMON_BOMB_MEDIUM_MP","ABILITY.BRITISH.TOMMY_HEAT_GRENADE_MP","ABILITY.BRITISH.TOMMY_MILLS_BOMB_MP","ABILITY.BRITISH.TOMMY_OFFICER_ARTILLERY","ABILITY.BRITISH.TOMMY_STAND_YOUR_GROUND","ABILITY.BRITISH.TUNE_UP_BONUS_MP","ABILITY.BRITISH.UEC_SELF_REPAIR","ABILITY.BRITISH.UEC_SELF_REPAIR_IMPROVED","ABILITY.BRITISH.UNIVERSAL_CARRIER_DROP_LMG","ABILITY.BRITISH.UNIVERSAL_CARRIER_DROP_PIAT","ABILITY.BRITISH.UNIVERSAL_CARRIER_VICKERS_SUPPRESSION_MP","ABILITY.BRITISH.VALENTINE_ARTILLERY_SEXTON_VICTOR_TARGET_CONCENTRATION_BARRAGE_MP","ABILITY.BRITISH.VALENTINE_SMOKE_BARRAGE_MP","ABILITY.BRITISH.VICKERS_AIRDROP","ABILITY.BRITISH.VICKERS_HMG_VET_1_BONUS","UPG.BRITISH.ABILITY_LOCK_OUT_17_POUNDER_ABILITY_ACTIVE","UPG.BRITISH.ABILITY_LOCK_OUT_AVRE_NOT_RELOADED","UPG.BRITISH.ABILITY_LOCK_OUT_AVRE_RELOADING","UPG.BRITISH.ABILITY_LOCK_OUT_BASE_ARTILLERY_COUNTER_BARRAGE_ABILITY_ACTIVE","UPG.BRITISH.ABILITY_LOCK_OUT_BASE_ARTILLERY_OVERWATCH_ABILITY_ACTIVE","UPG.BRITISH.ABILITY_LOCK_OUT_BOFORS_EMPLACEMENT_AA_MODE_ENABLED","UPG.BRITISH.ABILITY_LOCK_OUT_BOFORS_EMPLACEMENT_BARRAGE_ACTIVE","UPG.BRITISH.ABILITY_LOCK_OUT_GLIDER_CUSTOM_LOADOUT_LAUNCH_AVAILABLE","UPG.BRITISH.ABILITY_LOCK_OUT_GLIDER_HARD_LANDED","UPG.BRITISH.ABILITY_LOCK_OUT_GLIDER_NOT_STOPPED","UPG.BRITISH.ABILITY_LOCK_OUT_MORTAR_EMPLACEMENT_BARRAGE_ACTIVE","UPG.BRITISH.ABILITY_LOCK_OUT_MORTAR_EMPLACEMENT_SLOT_1_DEFAULT_LOADED","UPG.BRITISH.ABILITY_LOCK_OUT_MORTAR_EMPLACEMENT_SLOT_1_SPECIAL_1_LOADED","UPG.BRITISH.ABILITY_LOCK_OUT_MORTAR_EMPLACEMENT_SLOT_1_SPECIAL_2_LOADED","UPG.BRITISH.ADVANCED_ASSEMBLY","UPG.BRITISH.ADVANCED_ASSEMBLY_RESEARCH","UPG.BRITISH.ADVANCED_COVER","UPG.BRITISH.AEC_HE_ROUNDS_MP","UPG.BRITISH.AEC_HE_ROUNDS_UNLOCK_MP","UPG.BRITISH.AEC_RAPID_FIRE_MP","UPG.BRITISH.AEC_TARGET_OPTICS_MP","UPG.BRITISH.AEC_TARGET_TURRET_MP","UPG.BRITISH.AEC_TREAD_FIRST_SHOT_MP","UPG.BRITISH.AEC_TREAD_SECOND_SHOT_MP","UPG.BRITISH.ARTY_PIT_LOCKOUT_UPGRADE","UPG.BRITISH.ASSAULT","UPG.BRITISH.ASSAULT_ACTIVE","UPG.BRITISH.AVRE_MORTAR_RELOAD","UPG.BRITISH.BASE_BUILDING_BRACED_MP","UPG.BRITISH.BOYS_AT_RIFLE","UPG.BRITISH.BREN_LMG_UNLOCK_MP","UPG.BRITISH.BRITISH_TANK_COMMANDER","UPG.BRITISH.CAN_TUNE_UP_MP","UPG.BRITISH.COMMAND_HQ","UPG.BRITISH.COMMAND_VEHICLE","UPG.BRITISH.COMMAND_VEHICLE_ACTIVE","UPG.BRITISH.COMMAND_VEHICLE_ACTIVE_PLAYER","UPG.BRITISH.COMMANDO_RETREAT_SMOKE_DELAY","UPG.BRITISH.COMPANY_ANVIL_BUILDING_MP","UPG.BRITISH.COMPANY_ANVIL_MP","UPG.BRITISH.COMPANY_ANVIL_POINT_SIGHT_MP","UPG.BRITISH.COMPANY_HAMMER_BUILDING_MP","UPG.BRITISH.COMPANY_HAMMER_MP","UPG.BRITISH.COUNTER_BATTERY","UPG.BRITISH.COUNTER_BATTERY_MP","UPG.BRITISH.DEFENSIVE_OPERATIONS","UPG.BRITISH.EMPLACEMENT_DEACTIVATE_BRACE_DELAY","UPG.BRITISH.FIREFLY_TULIP_RELOAD","UPG.BRITISH.FIREFLY_TULIP_ROCKET","UPG.BRITISH.FLAMETHROWERS","UPG.BRITISH.FWD_HQ_RETREAT_MP","UPG.BRITISH.IMPROVED_FORTIFCATION","UPG.BRITISH.IMPROVED_FORTIFCATION_ASSSEMBLY_SQUAD","UPG.BRITISH.IMPROVED_FORTIFCATION_SQUAD","UPG.BRITISH.INFILTRATION_COMMANDOS","UPG.BRITISH.LAND_MATTRESS_FIRING","UPG.BRITISH.LAND_MATTRESS_LOADED_ROCKET","UPG.BRITISH.LAND_MATTRESS_LOADING_25LB_ROCKET","UPG.BRITISH.LAND_MATTRESS_LOADING_60LB_ROCKET","UPG.BRITISH.LAND_MATTRESS_LOADING_PHOS_ROCKET","UPG.BRITISH.PIAT","UPG.BRITISH.PIAT_UNLOCK_MP","UPG.BRITISH.PLATOON_AEC_RESEARCH_BUILDING_MP","UPG.BRITISH.PLATOON_AEC_RESEARCH_MP","UPG.BRITISH.PLATOON_BOFORS_RESEARCH_BUILDING_MP","UPG.BRITISH.PLATOON_BOFORS_RESEARCH_MP","UPG.BRITISH.PRECISION_BARRAGE","UPG.BRITISH.QF_25LB_COORDINATED_FIRE_MP","UPG.BRITISH.QF_25LB_COUNTER_BATTERY_MP","UPG.BRITISH.QF_25LB_FIRE_SUPPORT_BASE_MP","UPG.BRITISH.QF_25LB_FIRE_SUPPORT_MP","UPG.BRITISH.QF_25LB_HE_SHELL_MP","UPG.BRITISH.QF_25LB_RAPID_RESPONSE_DELAY_MP","UPG.BRITISH.QF_25LB_RAPID_RESPONSE_MP","UPG.BRITISH.QF_25LB_SHELL_TOGGLE_ABILITY_DELAY_MP","UPG.BRITISH.QF_25LB_TACTICAL_SUPPORT_BASE_MP","UPG.BRITISH.QF_25LB_TACTICAL_SUPPORT_MP","UPG.BRITISH.QF_25LB_TARGET_ACQUISITION_MP","UPG.BRITISH.REINFORCED_STRUCTURE","UPG.BRITISH.SAPPER_FLAMETHROWER","UPG.BRITISH.SAPPERS_HEAVY_SQUAD_MP","UPG.BRITISH.SAPPERS_MINESWEEPER_UPGRADE_MP","UPG.BRITISH.SNIPER_BOYS_AT_RIFLE","UPG.BRITISH.TANK_HUNTER_ACTIVE","UPG.BRITISH.TECH_STRUCTURE_1_CONSTRUCT_MP","UPG.BRITISH.TECH_STRUCTURE_1_MP","UPG.BRITISH.TECH_STRUCTURE_2_CONSTRUCT_MP","UPG.BRITISH.TECH_STRUCTURE_2_MP","UPG.BRITISH.TOMMY_BOYS_AT_RIFLES","UPG.BRITISH.TOMMY_INCREASED_SQUAD_SIZE_MP","UPG.BRITISH.TOMMY_MEDICAL_SUPPLIES","UPG.BRITISH.TOMMY_MILLS_BOMB_MP","UPG.BRITISH.TOMMY_PYROTECHNICS_SUPPLIES","UPG.BRITISH.UNIVERSAL_CARRIER_VICKERS_K_PACKAGE_UPGRADE_MP","UPG.BRITISH.UNIVERSAL_CARRIER_WASP_PACKAGE_UPGRADE_MP","UPG.BRITISH.VALENTINE_OBSERVATION_MODE_MP","UPG.BRITISH.WEAPON_RACK_UNLOCK_MP","EBP.GERMAN.ANTITANK_88MM_PAK43_SANDBAGS","EBP.GERMAN.ARMORED_CAR_SDKFZ_222","EBP.GERMAN.ARMORED_CAR_SDKFZ_222_MP","EBP.GERMAN.ASSAULT_GRENADIERS_LEADER_MP","EBP.GERMAN.ASSAULT_GRENADIERS_MP","EBP.GERMAN.ASSAULT_OFFICER","EBP.GERMAN.ASSAULT_OFFICER_GRENADIERS_BODYGUARD_MP","EBP.GERMAN.ASSAULT_OFFICER_MP","EBP.GERMAN.ATGUN_CREW","EBP.GERMAN.ATGUN_CREW_MP","EBP.GERMAN.ATGUN88_CREW","EBP.GERMAN.ATGUN88_CREW_MP","EBP.GERMAN.AXIS_BUNKER_STARTING_POSITION","EBP.GERMAN.AXIS_BUNKER_STARTING_POSITION_MP","EBP.GERMAN.BEREICH_FESTUNG","EBP.GERMAN.BEREICH_FESTUNG_MP","EBP.GERMAN.BRUMMBAR_STURMPANZER_IV_SDKFZ_166","EBP.GERMAN.BRUMMBAR_STURMPANZER_IV_SDKFZ_166_MP","EBP.GERMAN.BUNKER","EBP.GERMAN.BUNKER_MP","EBP.GERMAN.BUNKER_OF_DEATH_MP","EBP.GERMAN.CARGO_PLANE","EBP.GERMAN.CARGO_PLANE_1","EBP.GERMAN.CARGO_PLANE_FUEL","EBP.GERMAN.CARGO_PLANE_MUNITIONS","EBP.GERMAN.DOLCH_AKTIONEN","EBP.GERMAN.DOLCH_AKTIONEN_MP","EBP.GERMAN.ELEFANT_SDKFZ_184","EBP.GERMAN.ELEFANT_SDKFZ_184_MP","EBP.GERMAN.FUEL_POST_GERMAN","EBP.GERMAN.FUEL_POST_GERMAN_MP","EBP.GERMAN.GERMAN_BASE_STAMPER","EBP.GERMAN.GERMAN_HQ","EBP.GERMAN.GERMAN_HQ_MP","EBP.GERMAN.GERMAN_HQ_WRECK","EBP.GERMAN.GERMAN_HQ_WRECK_MP","EBP.GERMAN.GERMAN_MEDIC","EBP.GERMAN.GERMAN_MEDIC_MP","EBP.GERMAN.GERMAN_MINE","EBP.GERMAN.GERMAN_MINE_MP","EBP.GERMAN.GERMAN_SANDBAG_FENCE","EBP.GERMAN.GRANATEWERFER_34_81MM_MORTAR","EBP.GERMAN.GRANATEWERFER_34_81MM_MORTAR_MP","EBP.GERMAN.GRENADIERS","EBP.GERMAN.GRENADIERS_MP","EBP.GERMAN.GRENADIERS_SP","EBP.GERMAN.HACK_INVISI_PIONEER_MP","EBP.GERMAN.HALFTRACK_RIEGEL_43_MINE_MP","EBP.GERMAN.HALFTRACK_SDKFZ_251","EBP.GERMAN.HALFTRACK_SDKFZ_251_MP","EBP.GERMAN.HINTERE_PANZERWERK","EBP.GERMAN.HINTERE_PANZERWERK_MP","EBP.GERMAN.HINTERE_PANZERWERK_VORONEZH","EBP.GERMAN.HOWITZER_105MM_DUMMY","EBP.GERMAN.HOWITZER_105MM_LE_FH18","EBP.GERMAN.HOWITZER_105MM_LE_FH18_MP","EBP.GERMAN.HOWITZER_CREW","EBP.GERMAN.HOWITZER_CREW_MP","EBP.GERMAN.HULLDOWN_SANDBAG_WALL","EBP.GERMAN.HULLDOWN_SANDBAG_WALL_MP","EBP.GERMAN.INVISIBLE_RETREAT_POINT","EBP.GERMAN.LUFTWAFFE_OFFICER_TOW","EBP.GERMAN.M01_STUKA_DOGFIGHT","EBP.GERMAN.M01_STUKA_GROUND_ATTACK_FAST","EBP.GERMAN.MECHANIZED_250_HALFTRACK_GRENADIER_MP","EBP.GERMAN.MECHANIZED_250_HALFTRACK_MP","EBP.GERMAN.MG42_CREW","EBP.GERMAN.MG42_CREW_MP","EBP.GERMAN.MG42_CREW_SINGLE","EBP.GERMAN.MG42_HMG","EBP.GERMAN.MG42_HMG_ATTACK_GROUND","EBP.GERMAN.MG42_HMG_MP","EBP.GERMAN.MINE_FIELD","EBP.GERMAN.MINE_FIELD_BORDER","EBP.GERMAN.MINE_FIELD_BORDER_MP","EBP.GERMAN.MINE_FIELD_MINE","EBP.GERMAN.MINE_FIELD_MINE_M03","EBP.GERMAN.MINE_FIELD_MINE_MP","EBP.GERMAN.MINE_FIELD_MINE_TOW","EBP.GERMAN.MINE_FIELD_MP","EBP.GERMAN.MORTAR_CREW","EBP.GERMAN.MORTAR_CREW_MP","EBP.GERMAN.MORTAR_LIGHT_HALFTRACK_250_7","EBP.GERMAN.MORTAR_LIGHT_HALFTRACK_250_7_MP","EBP.GERMAN.MUNITION_POST_GERMAN","EBP.GERMAN.MUNITION_POST_GERMAN_MP","EBP.GERMAN.OFFICER","EBP.GERMAN.OFFICER_MP","EBP.GERMAN.OFFICER_TOW_OCCUPATION","EBP.GERMAN.OPEL_BLITZ_SUPPLY_TRUCK_MP","EBP.GERMAN.OPEL_BLITZ_TRUCK","EBP.GERMAN.OSTRUPPEN_SOLDIER","EBP.GERMAN.OSTRUPPEN_SOLDIER_MP","EBP.GERMAN.OSTWIND_FLAK_PANZER","EBP.GERMAN.OSTWIND_FLAK_PANZER_MP","EBP.GERMAN.PAK40_75MM_AT_GUN","EBP.GERMAN.PAK40_75MM_AT_GUN_MP","EBP.GERMAN.PAK43_88MM_AT_GUN","EBP.GERMAN.PAK43_88MM_AT_GUN_MP","EBP.GERMAN.PANTHER_SDKFZ_171","EBP.GERMAN.PANTHER_SDKFZ_171_MP","EBP.GERMAN.PANZER_GRENADIERS","EBP.GERMAN.PANZER_GRENADIERS_MP","EBP.GERMAN.PANZER_III_MP","EBP.GERMAN.PANZER_IV_COMMANDER_SDKFZ_161","EBP.GERMAN.PANZER_IV_COMMANDER_SDKFZ_161_MP","EBP.GERMAN.PANZER_IV_SDKFZ_161","EBP.GERMAN.PANZER_IV_SDKFZ_161_MP","EBP.GERMAN.PANZER_IV_SDKFZ_161_TUTORIAL","EBP.GERMAN.PANZER_IV_SDKFZ_AUSF1","EBP.GERMAN.PANZER_IV_SDKFZ_AUSF1_MP","EBP.GERMAN.PANZER_MG","EBP.GERMAN.PANZERWERFER_SDKFZ_4_1","EBP.GERMAN.PANZERWERFER_SDKFZ_4_1_MP","EBP.GERMAN.PARADROP_SNIPER_SOLDIER_MP","EBP.GERMAN.PIONEER","EBP.GERMAN.PIONEER_MP","EBP.GERMAN.PUMA_EAST_GERMAN","EBP.GERMAN.REPAIR_PIONEER","EBP.GERMAN.REPAIR_PIONEER_MP","EBP.GERMAN.RIEGEL_43_MINE","EBP.GERMAN.RIEGEL_43_MINE_MP","EBP.GERMAN.SCHWERES_KRIEGSWERK","EBP.GERMAN.SCHWERES_KRIEGSWERK_MP","EBP.GERMAN.SDKFZ_221_LIGHT_AT_HALFTRACK","EBP.GERMAN.SLIT_TRENCH_GERMAN","EBP.GERMAN.SLIT_TRENCH_GERMAN_MP","EBP.GERMAN.SNIPER_COVER","EBP.GERMAN.SNIPER_DIGIN_COVER_MP","EBP.GERMAN.SNIPER_SOLDIER","EBP.GERMAN.SNIPER_SOLDIER_MP","EBP.GERMAN.STORMTROOPERS_MP","EBP.GERMAN.STUG_III_E_SDKFZ_141_1","EBP.GERMAN.STUG_III_E_SDKFZ_141_1_COMMANDER_MP","EBP.GERMAN.STUG_III_E_SDKFZ_141_1_MP","EBP.GERMAN.STUG_III_G_SDKFZ_141_1","EBP.GERMAN.STUG_III_G_SDKFZ_141_1_MP","EBP.GERMAN.STUKA_AIR_RECON","EBP.GERMAN.STUKA_AIR_RECON_MP","EBP.GERMAN.STUKA_BOMBING_DIVE","EBP.GERMAN.STUKA_BOMBING_DIVE_MP","EBP.GERMAN.STUKA_BOMBING_RUN_SP","EBP.GERMAN.STUKA_FRAGEMENTATION_BOMB","EBP.GERMAN.STUKA_FRAGEMENTATION_BOMB_MP","EBP.GERMAN.STUKA_GROUND_ATTACK","EBP.GERMAN.STUKA_GROUND_ATTACK_LONG","EBP.GERMAN.STUKA_GROUND_ATTACK_M09","EBP.GERMAN.STUKA_GROUND_ATTACK_MP","EBP.GERMAN.STUKA_GROUND_ATTACK_WEST_AIRBORNE_ASSAULT","EBP.GERMAN.STUKA_INCENDIARY_BOMB","EBP.GERMAN.STUKA_INCENDIARY_BOMB_VICTORY","EBP.GERMAN.STUKA_JU87_ANTI_TANK","EBP.GERMAN.STUKA_JU87_ANTI_TANK_M06","EBP.GERMAN.STUKA_JU87_ANTI_TANK_MP","EBP.GERMAN.STUKA_JU87_ANTI_TANK_SUPERIORITY","EBP.GERMAN.STUKA_SMOKE_BOMB","EBP.GERMAN.STUKA_SMOKE_BOMB_MP","EBP.GERMAN.SUPPLY_TRUCK_METAL_M_01","EBP.GERMAN.SUPPLY_TRUCK_METAL_M_02","EBP.GERMAN.SUPPLY_TRUCK_MUNITIONS_CASE_AX_01","EBP.GERMAN.SUPPLY_TRUCK_MUNITIONS_CASE_AX_03","EBP.GERMAN.SUPPLY_TRUCK_SANDBAG_PILE_02","EBP.GERMAN.TACTICAL_BOMBER","EBP.GERMAN.TACTICAL_BOMBER_ACCURATE","EBP.GERMAN.TANK_TRAP","EBP.GERMAN.TELLER_MINE_MP","EBP.GERMAN.TIER1_MARKER","EBP.GERMAN.TIER2_MARKER","EBP.GERMAN.TIER3_MARKER","EBP.GERMAN.TIER4_MARKER","EBP.GERMAN.TIGER_ACE_SDKFZ_181_MP","EBP.GERMAN.TIGER_SDKFZ_181","EBP.GERMAN.TIGER_SDKFZ_181_MP","EBP.GERMAN.TIGER_SDKFZ_181_SINGLEPLAYER_MISSION","EBP.GERMAN.TIGER_SDKFZ_181_TOW","EBP.GERMAN.URBAN_ASSAULT_PANZER_GRENADIERS_MP","SBP.GERMAN.ASSAULT_GRENADIER_SQUAD_MP","SBP.GERMAN.ASSAULT_OFFICER_SQUAD","SBP.GERMAN.ASSAULT_OFFICER_SQUAD_MP","SBP.GERMAN.BRUMMBAR_SQUAD","SBP.GERMAN.BRUMMBAR_SQUAD_MP","SBP.GERMAN.CARGO_PLANE","SBP.GERMAN.CARGO_PLANE_FUEL","SBP.GERMAN.CARGO_PLANE_MUNITIONS","SBP.GERMAN.COMMAND_OFFICER_SQUAD_TOW","SBP.GERMAN.CONVOY_PIONEER_SQUAD","SBP.GERMAN.ELEFANT_TANK_DESTROYER_SQUAD","SBP.GERMAN.ELEFANT_TANK_DESTROYER_SQUAD_MP","SBP.GERMAN.GRENADIER_SQUAD","SBP.GERMAN.GRENADIER_SQUAD_M14","SBP.GERMAN.GRENADIER_SQUAD_MG42LMG_MP","SBP.GERMAN.GRENADIER_SQUAD_MP","SBP.GERMAN.GRENADIER_SQUAD_SP","SBP.GERMAN.HACK_INVISI_PIONEER_SQUAD_MP","SBP.GERMAN.HOWITZER_105MM_DUMMY_SQUAD","SBP.GERMAN.HOWITZER_105MM_LE_FH18_ARTILLERY","SBP.GERMAN.HOWITZER_105MM_LE_FH18_ARTILLERY_MP","SBP.GERMAN.LUFTWAFFE_OFFICER_SQUAD_TOW","SBP.GERMAN.M01_MG42_HEAVY_MACHINE_GUN_SQUAD_SINGLE","SBP.GERMAN.M01_STUKA_DOGFIGHT","SBP.GERMAN.M01_STUKA_GROUND_ATTACK_SQUAD_FAST","SBP.GERMAN.MECHANIZED_250_HALFTRACK_GRENADIERS_MP","SBP.GERMAN.MECHANIZED_250_HALFTRACK_MP","SBP.GERMAN.MECHANIZED_250_HALFTRACK_TOW","SBP.GERMAN.MG42_HEAVY_MACHINE_GUN_SQUAD","SBP.GERMAN.MG42_HEAVY_MACHINE_GUN_SQUAD_MP","SBP.GERMAN.MORTAR_250_HALFTRACK_SQUAD","SBP.GERMAN.MORTAR_250_HALFTRACK_SQUAD_MP","SBP.GERMAN.MORTAR_TEAM_81MM","SBP.GERMAN.MORTAR_TEAM_81MM_MP","SBP.GERMAN.OFFICER_SQUAD","SBP.GERMAN.OFFICER_SQUAD_MP","SBP.GERMAN.OPEL_BLITZ_SQUAD","SBP.GERMAN.OPEL_BLITZ_SUPPLY_SQUAD","SBP.GERMAN.OSTRUPPEN_SQUAD","SBP.GERMAN.OSTRUPPEN_SQUAD_M14","SBP.GERMAN.OSTRUPPEN_SQUAD_MP","SBP.GERMAN.OSTRUPPEN_SQUAD_RESERVES_MP","SBP.GERMAN.OSTWIND_SQUAD","SBP.GERMAN.OSTWIND_SQUAD_MP","SBP.GERMAN.PAK40_75MM_AT_GUN_SQUAD","SBP.GERMAN.PAK40_75MM_AT_GUN_SQUAD_MP","SBP.GERMAN.PAK43_88MM_AT_GUN_SQUAD","SBP.GERMAN.PAK43_88MM_AT_GUN_SQUAD_MP","SBP.GERMAN.PANTHER_SQUAD","SBP.GERMAN.PANTHER_SQUAD_MP","SBP.GERMAN.PANZER_GRENADIER_SQUAD","SBP.GERMAN.PANZER_GRENADIER_SQUAD_M14","SBP.GERMAN.PANZER_GRENADIER_SQUAD_MP","SBP.GERMAN.PANZER_IV_COMMAND_SQUAD","SBP.GERMAN.PANZER_IV_COMMAND_SQUAD_MP","SBP.GERMAN.PANZER_IV_SQUAD","SBP.GERMAN.PANZER_IV_SQUAD_MP","SBP.GERMAN.PANZER_IV_SQUAD_TUTORIAL","SBP.GERMAN.PANZER_IV_STUBBY_SQUAD","SBP.GERMAN.PANZER_IV_STUBBY_SQUAD_MP","SBP.GERMAN.PANZER_MG_SQUAD","SBP.GERMAN.PANZERWERFER_SQUAD","SBP.GERMAN.PANZERWERFER_SQUAD_MP","SBP.GERMAN.PARTISAN_SQUAD_M13","SBP.GERMAN.PIONEER_SQUAD","SBP.GERMAN.PIONEER_SQUAD_MP","SBP.GERMAN.PIONEER_SQUAD_TOW","SBP.GERMAN.PUMA_EAST_GERMAN_MP","SBP.GERMAN.SCOUTCAR_SDKFZ222","SBP.GERMAN.SCOUTCAR_SDKFZ222_MP","SBP.GERMAN.SDKFZ_221_LIGHT_AT_HALFTRACK","SBP.GERMAN.SDKFZ_251_HALFTRACK_SQUAD","SBP.GERMAN.SDKFZ_251_HALFTRACK_SQUAD_MP","SBP.GERMAN.SNIPER_SQUAD","SBP.GERMAN.SNIPER_SQUAD_MP","SBP.GERMAN.STORMTROOPER_SQUAD_MP","SBP.GERMAN.STUG_III_E_COMMANDER_SQUAD_MP","SBP.GERMAN.STUG_III_E_SQUAD","SBP.GERMAN.STUG_III_E_SQUAD_MP","SBP.GERMAN.STUG_III_SQUAD","SBP.GERMAN.STUG_III_SQUAD_MP","SBP.GERMAN.STUKA_AIR_CAP_SQUAD","SBP.GERMAN.STUKA_AIR_CAP_SQUAD_MP","SBP.GERMAN.STUKA_GROUND_ANTI_TANK_SQUAD","SBP.GERMAN.STUKA_GROUND_ANTI_TANK_SQUAD_M06","SBP.GERMAN.STUKA_GROUND_ANTI_TANK_SQUAD_MP","SBP.GERMAN.STUKA_GROUND_ANTI_TANK_SQUAD_SUPERIORITY","SBP.GERMAN.STUKA_GROUND_ATTACK_SQUAD","SBP.GERMAN.STUKA_GROUND_ATTACK_SQUAD_LONG","SBP.GERMAN.STUKA_GROUND_ATTACK_SQUAD_M09","SBP.GERMAN.STUKA_GROUND_ATTACK_SQUAD_MP","SBP.GERMAN.STUKA_GROUND_ATTACK_WEST_GERMANS_SQUAD","SBP.GERMAN.STUKA_GROUND_FRAGMENTATION_SQUAD","SBP.GERMAN.STUKA_GROUND_FRAGMENTATION_SQUAD_MP","SBP.GERMAN.STUKA_INCENDIARY_BOMB_SQUAD","SBP.GERMAN.STUKA_INCENDIARY_BOMB_VICTORY_SQUAD","SBP.GERMAN.STUKA_SMOKE_SQUAD","SBP.GERMAN.STUKA_SMOKE_SQUAD_MP","SBP.GERMAN.TACTICAL_BOMBERS","SBP.GERMAN.TACTICAL_BOMBERS_ACCURATE","SBP.GERMAN.TIGER_ACE_SQUAD_MP","SBP.GERMAN.TIGER_SQUAD","SBP.GERMAN.TIGER_SQUAD_MP","SBP.GERMAN.TIGER_SQUAD_SP_A2_M02","SBP.GERMAN.TIGER_SQUAD_TOW","SBP.GERMAN.URBAN_ASSAULT_PANZER_GRENADIER_SQUAD_MP","ABILITY.GERMAN.AIR_DROPPED_MEDICAL_SUPPLIES","ABILITY.GERMAN.AIR_DROPPED_MUNITIONS","ABILITY.GERMAN.AMBUSH_CAMO_HOLD_FIRE_MP","ABILITY.GERMAN.AMBUSH_CAMOUFLAGE","ABILITY.GERMAN.AMBUSH_CAMOUFLAGE_AT","ABILITY.GERMAN.AMBUSH_CAMOUFLAGE_MORTAR","ABILITY.GERMAN.AMBUSH_CAMOUFLAGE_PIO","ABILITY.GERMAN.AMBUSH_CAMOUFLAGE_UNLOCK","ABILITY.GERMAN.AMBUSH_CAMOUFLAGE_UPGRADE","ABILITY.GERMAN.ARMOR_COMMANDER","ABILITY.GERMAN.ASSAULT_FIELD_OFFICER","ABILITY.GERMAN.ASSAULT_GRENADIER_GRENADE","ABILITY.GERMAN.ASSAULT_GRENADIER_SPRINT_MP","ABILITY.GERMAN.ASSAULT_GRENADIERS","ABILITY.GERMAN.ASSAULT_OFFICER_INSPIRATION","ABILITY.GERMAN.ASSAULT_OFFICER_INSPIRATION_VET3","ABILITY.GERMAN.ASSAULT_OFFICER_VICTOR_TARGET","ABILITY.GERMAN.AXIS_SNIPER_DELAYED_COVER_AUTO_CAMOUFLAGE_MP","ABILITY.GERMAN.BLINDING_GRENADE","ABILITY.GERMAN.BLINDING_GRENADES_UNLOCK","ABILITY.GERMAN.BREAKTHROUGH","ABILITY.GERMAN.BRUMMBAR_BUNKER_BUSTER_MP","ABILITY.GERMAN.BRUMMBAR_CRITICAL_SHOTS_MP","ABILITY.GERMAN.BRUMMBAR_HOLD_FIRE_MP","ABILITY.GERMAN.CAMOUFLAGE_NETS","ABILITY.GERMAN.CAMOUFLAGE_NETS_UNLOCK","ABILITY.GERMAN.COMMAND_PANTHER_MARK_TARGET","ABILITY.GERMAN.CONVERT_TANK_WRECK","ABILITY.GERMAN.CONVERT_TANK_WRECK_UNLOCK","ABILITY.GERMAN.COUNTERATTACK_TACTICS","ABILITY.GERMAN.CRUSH_THE_POCKET","ABILITY.GERMAN.DEFENSIVE_FORTIFICATIONS","ABILITY.GERMAN.ELEFANT_CRITICAL_SHOTS_MP","ABILITY.GERMAN.ELEFANT_UNLOCK","ABILITY.GERMAN.ELEPHANT_CONE_LOS_TOGGLE_ABILITY","ABILITY.GERMAN.ELEPHANT_CONE_LOS_TOGGLE_ABILITY_MP","ABILITY.GERMAN.FAST_MARCH","ABILITY.GERMAN.FATALITY_PANZERWERFER_BARRAGE","ABILITY.GERMAN.FATALITY_SMOKE_BARRAGE","ABILITY.GERMAN.FATALITY_STUKA_INCENDIARY_AIRSTRIKE","ABILITY.GERMAN.FATALITY_STUKA_SMOKE_STRAFE_AIRSTRIKE","ABILITY.GERMAN.FORWARD_REPAIR_STATION","ABILITY.GERMAN.GERMAN_HQ_PIONEER_CALL_IN","ABILITY.GERMAN.GERMAN_HULLDOWN_ABILITY","ABILITY.GERMAN.GERMAN_HULLDOWN_DISABLE","ABILITY.GERMAN.GERMAN_MORTAR_HOLD_FIRE","ABILITY.GERMAN.GERMAN_MORTAR_HOLD_FIRE_MP","ABILITY.GERMAN.GERMAN_SALVAGE_ABILITY","ABILITY.GERMAN.GERMAN_SALVAGE_ABILITY_CONVOY","ABILITY.GERMAN.GERMAN_SALVAGE_ABILITY_MP","ABILITY.GERMAN.GERMAN_WARNING_SMOKE","ABILITY.GERMAN.GOLIATH_IN_COVER_AUTO_CAMOUFLAGE_MP","ABILITY.GERMAN.GOLIATH_SELF_DESTRUCT_MP","ABILITY.GERMAN.GRENADIER_ANTITANK_RIFLE_GRENADE_ABILITY","ABILITY.GERMAN.GRENADIER_ANTITANK_RIFLE_GRENADE_ABILITY_MP","ABILITY.GERMAN.GRENADIER_PANZERFAUST","ABILITY.GERMAN.GRENADIER_PANZERFAUST_MP","ABILITY.GERMAN.GRENADIER_RIFLE_GRENADE_ABILITY","ABILITY.GERMAN.GRENADIER_RIFLE_GRENADE_ABILITY_MP","ABILITY.GERMAN.GRENADIER_RIFLE_GRENADE_ABILITY_TUTORIAL","ABILITY.GERMAN.HALFTRACK_INCENDIARY_MORTAR_BARRAGE","ABILITY.GERMAN.HALFTRACK_INCENDIARY_MORTAR_BARRAGE_MP","ABILITY.GERMAN.HALFTRACK_MORTAR_BARRAGE","ABILITY.GERMAN.HALFTRACK_MORTAR_BARRAGE_MP","ABILITY.GERMAN.HALFTRACK_MORTAR_VICTORTARGET_BARRAGE_MP","ABILITY.GERMAN.HALFTRACK_SMOKE_BARRAGE","ABILITY.GERMAN.HALFTRACK_SMOKE_BARRAGE_MP","ABILITY.GERMAN.HEAVY_AT_MINE_UNLOCK","ABILITY.GERMAN.HOWITZER_105MM_BARRAGE_ABILITY","ABILITY.GERMAN.HOWITZER_105MM_BARRAGE_ABILITY_MP","ABILITY.GERMAN.HOWITZER_105MM_BARRAGE_VET3_ABILITY_MP","ABILITY.GERMAN.HOWITZER_105MM_EMPLACEMENT_UNLOCK","ABILITY.GERMAN.HOWITZER_105MM_VICTORTARGET_BARRAGE_ABILITY_MP","ABILITY.GERMAN.HOWITZER_COUNTER_BARRAGE_ATTACK_MP","ABILITY.GERMAN.HOWITZER_COUNTER_BARRAGE_MP","ABILITY.GERMAN.HOWITZER_DEFAULT_REFACE_ACTION","ABILITY.GERMAN.HULL_DOWN_UNLOCK","ABILITY.GERMAN.INFANTRY_MEDKITS","ABILITY.GERMAN.INFANTRY_MEDKITS_MP","ABILITY.GERMAN.JAEGER_INFANTRY_UNLOCK","ABILITY.GERMAN.JAEGER_INTERROGATION","ABILITY.GERMAN.LAY_HEAVY_AT_MINE","ABILITY.GERMAN.LIGHT_SUPPORT_ARTILLERY","ABILITY.GERMAN.MECHANIZED_ASSAULT_GROUP","ABILITY.GERMAN.MECHANIZED_GRENADIER_GROUP","ABILITY.GERMAN.MG42_CAMO_HOLD_FIRE_MP","ABILITY.GERMAN.MG42_PHOSPHORUS_ROUNDS","ABILITY.GERMAN.MG42_PHOSPHORUS_ROUNDS_MP","ABILITY.GERMAN.MORTAR_COUNTER_BARRAGE_ATTACK_MP","ABILITY.GERMAN.MORTAR_COUNTER_BARRAGE_MP","ABILITY.GERMAN.MORTAR_COUNTER_BARRAGE_WEAPON_MP","ABILITY.GERMAN.MORTAR_HALFTRACK","ABILITY.GERMAN.MORTAR_INCENDIARY_BARRAGE","ABILITY.GERMAN.MORTAR_TEAM_INCENDIARY_BARRAGE_MP","ABILITY.GERMAN.MORTAR_TEAM_MORTAR_BARRAGE","ABILITY.GERMAN.MORTAR_TEAM_MORTAR_BARRAGE_MP","ABILITY.GERMAN.MORTAR_TEAM_MORTAR_VICTORTARGET_BARRAGE_MP","ABILITY.GERMAN.MORTAR_TEAM_SMOKE_BARRAGE","ABILITY.GERMAN.MORTAR_TEAM_SMOKE_BARRAGE_MP","ABILITY.GERMAN.MUNITIONS_BLITZ","ABILITY.GERMAN.OFFICER_SMOKE_ARTILLERY","ABILITY.GERMAN.OPEL_SUPPLY_TERRITORY_CHECK","ABILITY.GERMAN.OSTRUPPEN","ABILITY.GERMAN.OSTRUPPEN_COVER_BONUS","ABILITY.GERMAN.OSTRUPPEN_RESERVES","ABILITY.GERMAN.PAK_43_EMPLACEMENT_UNLOCK","ABILITY.GERMAN.PAK40_CRITICAL_SHOTS_MP","ABILITY.GERMAN.PAK40_TARGET_WEAK_POINT_MP","ABILITY.GERMAN.PAK43_CRITICAL_SHOTS_MP","ABILITY.GERMAN.PAK43_TARGET_WEAK_POINT_MP","ABILITY.GERMAN.PANTHER_TIGER_BLITZKRIEG_MP","ABILITY.GERMAN.PANZER_COMMANDER_AURA_MP","ABILITY.GERMAN.PANZER_DEFENSIVE_SMOKE","ABILITY.GERMAN.PANZER_GRENADIER_BUNDLED_CAMPAIGN","ABILITY.GERMAN.PANZER_GRENADIER_BUNDLED_GRENADE","ABILITY.GERMAN.PANZER_GRENADIER_BUNDLED_GRENADE_MP","ABILITY.GERMAN.PANZER_GRENADIER_BUNDLED_TUTORIAL","ABILITY.GERMAN.PANZER_PANTHER_TIGER_DEFENSIVE_SMOKE_TOW","ABILITY.GERMAN.PANZER_PANTHER_TIGER_OSTWIND_BLITZKRIEG","ABILITY.GERMAN.PANZER_PANTHER_TIGER_OSTWIND_BLITZKRIEG_MP","ABILITY.GERMAN.PANZER_PANTHER_TIGER_OSTWIND_BLITZKRIEG_TOW","ABILITY.GERMAN.PANZER_PANTHER_TIGER_OSTWIND_FLARES_ABILITY","ABILITY.GERMAN.PANZER_PANTHER_TIGER_OSTWIND_REPAIR_TOW","ABILITY.GERMAN.PANZER_TACTICIAN_UNLOCK","ABILITY.GERMAN.PANZERWERFER_COUNTER_BARRAGE_ATTACK_MP","ABILITY.GERMAN.PANZERWERFER_COUNTER_BARRAGE_MP","ABILITY.GERMAN.PANZERWERFER_ROCKET_BARRAGE","ABILITY.GERMAN.PANZERWERFER_ROCKET_BARRAGE_MP","ABILITY.GERMAN.PANZERWERFER_ROCKET_VICTORTARGET_BARRAGE_MP","ABILITY.GERMAN.PIONEER_BARBED_WIRE_CUTTING_ABILITY","ABILITY.GERMAN.PIONEER_BARBED_WIRE_CUTTING_ABILITY_MP","ABILITY.GERMAN.PIONEER_FLAMETHROWER","ABILITY.GERMAN.PUMA_CRITICAL_SHOTS_MP","ABILITY.GERMAN.PUMA_DISPATCH","ABILITY.GERMAN.RAILWAY_GUN_ARTILLERY","ABILITY.GERMAN.REDISTRIBUTE_RESOURCES","ABILITY.GERMAN.RELIEF_INFANTRY","ABILITY.GERMAN.REMOVE_AMBUSH_CAMOUFLAGE","ABILITY.GERMAN.RESOURCE_REQUISITION","ABILITY.GERMAN.RETREAT_TO_FORWARD_HQ","ABILITY.GERMAN.SCOUT_CAR_HALFTRACK_INFANTRY_AWARENESS","ABILITY.GERMAN.SCOUT_CAR_HALFTRACK_INFANTRY_AWARENESS_MP","ABILITY.GERMAN.SDKFZ_221_LIGHT_AT_HALFTRACK","ABILITY.GERMAN.SECTOR_ARTILLERY","ABILITY.GERMAN.SNIPER_INCENDIARY_ROUND_MP","ABILITY.GERMAN.SPRINT","ABILITY.GERMAN.STATIONARY_LOS_UNLOCK","ABILITY.GERMAN.STORMTROOPER_ASSAULT_AMBUSH_MP","ABILITY.GERMAN.STORMTROOPER_SPRINT_MP","ABILITY.GERMAN.STORMTROOPER_TANK_DETECTION_MP","ABILITY.GERMAN.STORMTROOPERS","ABILITY.GERMAN.STRATEGIC_BOMBING","ABILITY.GERMAN.STUG_CRITICAL_SHOTS_MP","ABILITY.GERMAN.STUG_ELEFANT_PAK40_PAK43_BRUMMBAR_CRITICAL_SHOTS","ABILITY.GERMAN.STUG_ELEFANT_PAK40_PAK43_BRUMMBAR_CRITICAL_SHOTS_MP","ABILITY.GERMAN.STUG_III_E","ABILITY.GERMAN.STUKA_AERIAL_SUPERIORITY_CLOSE_AIR_SUPPORT","ABILITY.GERMAN.STUKA_AERIAL_SUPERIORITY_RECON","ABILITY.GERMAN.STUKA_AERIAL_SUPERIORITY_STRAFING_RUN","ABILITY.GERMAN.STUKA_AIR_RECON","ABILITY.GERMAN.STUKA_BOMBING_RUN_SP","ABILITY.GERMAN.STUKA_BOMBING_STRIKE","ABILITY.GERMAN.STUKA_BOMBING_STRIKE_TOW","ABILITY.GERMAN.STUKA_CLOSE_AIR_M06","ABILITY.GERMAN.STUKA_CLOSE_AIR_M06_MP","ABILITY.GERMAN.STUKA_CLOSE_AIR_SUPPORT","ABILITY.GERMAN.STUKA_FRAGMENTATION_BOMB","ABILITY.GERMAN.STUKA_INCENDIARY_BOMBS","ABILITY.GERMAN.STUKA_SMOKE_BOMB","ABILITY.GERMAN.STUKA_STRAFING_RUN","ABILITY.GERMAN.SUPPLY_BREAK","ABILITY.GERMAN.SUPPLY_TRUCK","ABILITY.GERMAN.SUPPLY_TRUCK_LOCKDOWN","ABILITY.GERMAN.SUPPORT_TEAM_AMBUSH_CAMOUFLAGE","ABILITY.GERMAN.TANK_AWARENESS_UNLOCK","ABILITY.GERMAN.TANK_DETECTION_ABILITY_CONVOY","ABILITY.GERMAN.TIGER_ACE_CRITICAL_SHOTS_MP","ABILITY.GERMAN.TIGER_TANK","ABILITY.GERMAN.TIGER_TANK_ACE","ABILITY.GERMAN.TRENCH_UNLOCK","ABILITY.GERMAN.TROOP_TRAINING","ABILITY.GERMAN.URBAN_ASSAULT_GRENADIERS","ABILITY.GERMAN.URBAN_ASSAULT_SATCHEL_CHARGE_THROW_ABILITY_MP","ABILITY.GERMAN.URBAN_ASSAULT_SMOKE_GRENADE","ABILITY.GERMAN.URBAN_ASSAULT_SMOKE_GRENADE_2","ABILITY.GERMAN.WEHR_VEHICLE_HOLD_FIRE_MP","UPG.GERMAN.AERIAL_SUPERIORITY_RECON_PLANE","UPG.GERMAN.AERIAL_SUPERIORITY_STUKA_CLOSE_AIR_SUPPORT","UPG.GERMAN.AERIAL_SUPERIORITY_STUKA_STRAFE","UPG.GERMAN.AIR_DROP_MEDICAL_SUPPLIES","UPG.GERMAN.AIR_DROP_RESOURCES","UPG.GERMAN.AMBUSH_CAMOU_PACKAGE","UPG.GERMAN.AMBUSH_CAMOUFLAGE","UPG.GERMAN.ARMOR_COMMANDER","UPG.GERMAN.ASSAULT_ARCHETYPE","UPG.GERMAN.ASSAULT_FIELD_OFFICER","UPG.GERMAN.ASSAULT_GRENADIERS","UPG.GERMAN.BATTLE_PHASE_2","UPG.GERMAN.BATTLE_PHASE_2_MP","UPG.GERMAN.BATTLE_PHASE_3","UPG.GERMAN.BATTLE_PHASE_3_MP","UPG.GERMAN.BATTLE_PHASE_4","UPG.GERMAN.BATTLE_PHASE_4_MP","UPG.GERMAN.BLINDING_GRENADES","UPG.GERMAN.BREAKTHROUGH","UPG.GERMAN.BRUMMBAR_TOP_GUNNER","UPG.GERMAN.BRUMMBAR_TOP_GUNNER_MP","UPG.GERMAN.BUNKER_COMMAND","UPG.GERMAN.BUNKER_COMMAND_MP","UPG.GERMAN.BUNKER_MEDIC_STATION","UPG.GERMAN.BUNKER_MEDIC_STATION_MP","UPG.GERMAN.BUNKER_MG42_ADDITION","UPG.GERMAN.BUNKER_MG42_ADDITION_MP","UPG.GERMAN.CAMOUFLAGE_NET_ACTIVATED","UPG.GERMAN.CAMOUFLAGE_NETS","UPG.GERMAN.CAN_CAMOUFLAGE","UPG.GERMAN.COUNTERATTACK_TACTICS","UPG.GERMAN.CRUSH_THE_POCKET","UPG.GERMAN.DEFENSIVE_FORTIFICATIONS","UPG.GERMAN.ELEFANT_UNLOCK","UPG.GERMAN.FAST_MARCH","UPG.GERMAN.FESTUNG_ARCHETYPE","UPG.GERMAN.FORWARD_REPAIR_STATION","UPG.GERMAN.GRENADIER_MG42_LMG","UPG.GERMAN.GRENADIER_MG42_LMG_MP","UPG.GERMAN.HEAVY_AT_MINE","UPG.GERMAN.HOWITZER_105MM_EMPLACEMENT","UPG.GERMAN.HOWITZER_COUNTER_BARRAGE_COOLDOWN_MP","UPG.GERMAN.HULL_DOWN","UPG.GERMAN.HULLDOWN_ACTIVATED","UPG.GERMAN.HULLDOWN_CONSTRUCTING","UPG.GERMAN.JAEGER_ARCHETYPE","UPG.GERMAN.JAEGER_LIGHT_INFANTRY","UPG.GERMAN.LIGHT_ARTILLERY_SUPPORT","UPG.GERMAN.LIGHT_INFANTRY_PACKAGE","UPG.GERMAN.LIGHT_INFANTRY_PANZERGREN_PACKAGE","UPG.GERMAN.MECHANIZED_GRENADIER_GROUP","UPG.GERMAN.MECHANIZED_GROUP","UPG.GERMAN.MG42_HOLDFIRE_CAMOUFLAGE_NET_ACTIVATED","UPG.GERMAN.MORTAR_COUNTER_BARRAGE_COOLDOWN_MP","UPG.GERMAN.MORTAR_COUNTER_BARRAGE_MP","UPG.GERMAN.MORTAR_HALFTRACK","UPG.GERMAN.MORTAR_HALFTRACK_250_UPGRADE","UPG.GERMAN.MORTAR_HALFTRACK_COUNTER_BARRAGE_COOLDOWN_MP","UPG.GERMAN.MORTAR_INCENDIARY_BARRAGE","UPG.GERMAN.MUNITION_BLITZ","UPG.GERMAN.OSTRUPPEN","UPG.GERMAN.OSTRUPPEN_RESERVES","UPG.GERMAN.PAK_43_EMPLACEMENT","UPG.GERMAN.PANTHER_TOP_GUNNER","UPG.GERMAN.PANTHER_TOP_GUNNER_MP","UPG.GERMAN.PANZER_GRENADIER_PANZERSHRECK_ATW_ITEM","UPG.GERMAN.PANZER_GRENADIER_PANZERSHRECK_ATW_ITEM_1_SCHREK_MP","UPG.GERMAN.PANZER_GRENADIER_PANZERSHRECK_ATW_ITEM_MP","UPG.GERMAN.PANZER_GRENADIER_PANZERSHRECK_ATW_ITEM_SECOND","UPG.GERMAN.PANZER_GRENADIER_PANZERSHRECK_ATW_ITEM_SECOND_MP","UPG.GERMAN.PANZER_GRENADIER_PANZERSHRECK_ATW_ITEM_THIRD_MP","UPG.GERMAN.PANZER_TACTICIAN","UPG.GERMAN.PANZER_TOP_GUNNER","UPG.GERMAN.PANZER_TOP_GUNNER_MP","UPG.GERMAN.PANZERBUSCHE_39","UPG.GERMAN.PANZERBUSCHE_39_MP","UPG.GERMAN.PANZERWERFER_COUNTER_BARRAGE_COOLDOWN_MP","UPG.GERMAN.PIONEER_FLAMETHROWER","UPG.GERMAN.PIONEER_FLAMETHROWER_MP","UPG.GERMAN.PIONEER_MINESWEEPER","UPG.GERMAN.PIONEER_MINESWEEPER_MP","UPG.GERMAN.PUMA_DISPATCH","UPG.GERMAN.RAILWAY_ARTILLERY_SUPPORT","UPG.GERMAN.RECON_PLANE","UPG.GERMAN.REDISTRIBUTE_RESOURCES","UPG.GERMAN.RELIEF_INFANTRY","UPG.GERMAN.SDKFZ_222_20MM_GUN","UPG.GERMAN.SDKFZ_222_20MM_GUN_MP","UPG.GERMAN.SDKFZ_251_HALFTRACK_FLAMMPANZERWAGEN_UPGRADE","UPG.GERMAN.SDKFZ_251_HALFTRACK_FLAMMPANZERWAGEN_UPGRADE_MP","UPG.GERMAN.SDKFZ_251_HALFTRACK_MOBILE_MEDIC_STATION_UPGRADE","UPG.GERMAN.SECTOR_ARTILLERY","UPG.GERMAN.SPRINT","UPG.GERMAN.STATIONARY_LOS_GAIN","UPG.GERMAN.STORMTROOPER_ANTITANK_PACKAGE_MP","UPG.GERMAN.STORMTROOPER_ASSAULT_PACKAGE_MP","UPG.GERMAN.STORMTROOPER_PANZERSCHRECK_MP","UPG.GERMAN.STORMTROOPERS","UPG.GERMAN.STRATEGIC_BOMBING","UPG.GERMAN.STUG_III_E_UNLOCK","UPG.GERMAN.STUG_SHORT_BARREL","UPG.GERMAN.STUG_TOP_GUNNER","UPG.GERMAN.STUG_TOP_GUNNER_MP","UPG.GERMAN.STUKA_BOMBING_RUN_UPGRADE","UPG.GERMAN.STUKA_CLOSE_AIR_SUPPORT","UPG.GERMAN.STUKA_FLAME_STRIKE","UPG.GERMAN.STUKA_FRAGMENTATION_BOMB","UPG.GERMAN.STUKA_SMOKE_BOMB","UPG.GERMAN.STUKA_STRAFE","UPG.GERMAN.SUPPLY_BREAK","UPG.GERMAN.SUPPLY_TRUCK_ACTIVE","UPG.GERMAN.SUPPLY_TRUCK_EXIT","UPG.GERMAN.SUPPLY_TRUCK_FILL_STATE","UPG.GERMAN.SUPPLY_TRUCK_FULL","UPG.GERMAN.SUPPLY_TRUCK_LOCKDOWN","UPG.GERMAN.TANK_AWARENESS","UPG.GERMAN.TIGER_TANK","UPG.GERMAN.TIGER_TANK_ACE","UPG.GERMAN.TIGER_TANK_ACE_CALLIN_RESTRICTION","UPG.GERMAN.TIGER_TOP_GUNNER","UPG.GERMAN.TIGER_TOP_GUNNER_MP","UPG.GERMAN.TIGER_TOP_GUNNER_TOW","UPG.GERMAN.TOW_1941_GERMAN","UPG.GERMAN.TRENCH","UPG.GERMAN.TROOP_TRAINING","UPG.GERMAN.URBAN_ASSAULT_ARMOR_UPGRADE","UPG.GERMAN.URBAN_ASSAULT_PANZER_GRENADIERS","UPG.GERMAN.URBAN_ASSAULT_PANZER_GRENADIERS_FLAMETHROWER_MP","UPG.GERMAN.VEHICLES_OPTICS","UPG.GERMAN.XP1_GERMAN_DEMO_UPGRADE","EBP.PROXY.PROXY_MEDIC_MP","EBP.PROXY.PROXY_RIFLEMAN_SOLDIER_A","EBP.PROXY.PROXY_RIFLEMAN_SOLDIER_B","EBP.PROXY.PROXY_RIFLEMAN_SOLDIER_C","EBP.PROXY.PROXY_SNIPER_RECON_MP","SBP.PROXY.PROXY_HMG_SQUAD_MP","SBP.PROXY.PROXY_MECH_SQUAD_MP","SBP.PROXY.PROXY_RIFLEMEN_SQUAD_MP","SBP.PROXY.PROXY_SNIPER_SQUAD_MP","EBP.SOVIET._CIVILIAN_FEMALE","EBP.SOVIET._CIVILIAN_FEMALE_MP","EBP.SOVIET._CIVILIAN_MALE","EBP.SOVIET._CIVILIAN_MALE_MP","EBP.SOVIET.ANTI_PERSONNEL_MINES","EBP.SOVIET.ARTILLERY_203MM_B4","EBP.SOVIET.ATGUN53K_CREW","EBP.SOVIET.ATGUN53K_CREW_MP","EBP.SOVIET.ATGUNZIS_CREW","EBP.SOVIET.ATGUNZIS_CREW_MP","EBP.SOVIET.BARBED_WIRE_FENCE","EBP.SOVIET.BARBED_WIRE_FENCE_MP","EBP.SOVIET.BARBED_WIRE_FIELD","EBP.SOVIET.BARBED_WIRE_FIELD_MP","EBP.SOVIET.BARRACKS","EBP.SOVIET.BARRACKS_MP","EBP.SOVIET.BASE_CONSCRIPT_SOLDIER","EBP.SOVIET.BASE_CONSCRIPT_SOLDIER_MP","EBP.SOVIET.BOAT_01_ENTITY","EBP.SOVIET.CARGO_PLANE_SOVIET","EBP.SOVIET.COMBAT_ENGINEER","EBP.SOVIET.COMBAT_ENGINEER_MP","EBP.SOVIET.COMMISSAR","EBP.SOVIET.COMMISSAR_227","EBP.SOVIET.COMMISSAR_MP","EBP.SOVIET.COMMISSAR_OF_DEATH_227_MP","EBP.SOVIET.CONSCRIPT_SOLDIER","EBP.SOVIET.CONSCRIPT_SOLDIER_CONSCRIPT_BODYGUARD_MP","EBP.SOVIET.CONSCRIPT_SOLDIER_MP","EBP.SOVIET.DHSK_38_MACHINE_GUN","EBP.SOVIET.DHSK_38_MACHINE_GUN_MP","EBP.SOVIET.DSHK_WEAPON_CREW","EBP.SOVIET.DSHK_WEAPON_CREW_MP","EBP.SOVIET.FLARE_FIRE_MP","EBP.SOVIET.FLARE_MINE","EBP.SOVIET.FLARE_MINE_MP","EBP.SOVIET.FORWARD_HQ","EBP.SOVIET.GUARD_TROOPS","EBP.SOVIET.GUARD_TROOPS_ASSAULT_MP","EBP.SOVIET.GUARD_TROOPS_MP","EBP.SOVIET.HM_120_38_MORTAR","EBP.SOVIET.HM_120_38_MORTAR_MP","EBP.SOVIET.HOWITZER_CREW_SOVIET","EBP.SOVIET.HOWITZER_CREW_SOVIET_MP","EBP.SOVIET.HOWITZER_CREW203__SOVIET_MP","EBP.SOVIET.HQ","EBP.SOVIET.HQ_INVISIBLE_SP","EBP.SOVIET.HQ_MP","EBP.SOVIET.HQ_NO_WRECK","EBP.SOVIET.HQ_WRECK","EBP.SOVIET.HQ_WRECK_M06","EBP.SOVIET.HQ_WRECK_MP","EBP.SOVIET.IL_2_STURMOVIK","EBP.SOVIET.IL_2_STURMOVIK_ADVANCED_MP","EBP.SOVIET.IL_2_STURMOVIK_ANTI_TANK_BOMB_MP","EBP.SOVIET.IL_2_STURMOVIK_MARK_VEHICLE_MP","EBP.SOVIET.IL_2_STURMOVIK_MP","EBP.SOVIET.IL_2_STURMOVIK_RECON","EBP.SOVIET.IL_2_STURMOVIK_RECON_MP","EBP.SOVIET.IL_2_STURMOVIK_ROCKET","EBP.SOVIET.IL_2_STURMOVIK_ROCKET_MP","EBP.SOVIET.IL_2_STURMOVIK_ROCKET_SP","EBP.SOVIET.IL_2_STURMOVIK_VICTORY_MP","EBP.SOVIET.IS_2_HEAVY_TANK","EBP.SOVIET.IS_2_HEAVY_TANK_MP","EBP.SOVIET.ISAKOVICH_A01_COMMANDER","EBP.SOVIET.ISAKOVICH_M06","EBP.SOVIET.ISU_152_SPG","EBP.SOVIET.ISU_152_SPG_MP","EBP.SOVIET.KATYUSHA_BM_13N","EBP.SOVIET.KATYUSHA_BM_13N_MP","EBP.SOVIET.KV_1","EBP.SOVIET.KV_1_COMMANDER_MP","EBP.SOVIET.KV_1_MP","EBP.SOVIET.KV_2","EBP.SOVIET.KV_2_MP","EBP.SOVIET.KV_2_TOW","EBP.SOVIET.KV_8","EBP.SOVIET.KV_8_MP","EBP.SOVIET.LIGHT_ANTI_VEHICLE_MINES","EBP.SOVIET.M01_BASE_CONSCRIPT_SOLDIER","EBP.SOVIET.M01_BASE_CONSCRIPT_SOLDIER_DURABLE","EBP.SOVIET.M01_CONSCRIPT_SOLDIER","EBP.SOVIET.M01_CONSCRIPT_SOLDIER_DOCK","EBP.SOVIET.M01_CONSCRIPT_SOLDIER_HARMLESS","EBP.SOVIET.M01_CONSCRIPT_SOLDIER_HARMLESS_DURABLE","EBP.SOVIET.M01_IL_2_STURMOVIK_ROCKET","EBP.SOVIET.M01_IL2_DOGFIGHT","EBP.SOVIET.M01_MEDIC","EBP.SOVIET.M08_T_34_76_SMALLPATH","EBP.SOVIET.M08_TANK_BUSTER_CONSCRIPT","EBP.SOVIET.M11_ANIA_SNIPER","EBP.SOVIET.M11_ISAKOVICH_RECON","EBP.SOVIET.M11_PARTISAN_TROOP_KAR98K","EBP.SOVIET.M11_PARTISAN_TROOP_NAGANT","EBP.SOVIET.M11_PARTISAN_TROOP_NOWEAPON","EBP.SOVIET.M11_SNIPER","EBP.SOVIET.M11_SNIPER_RECON","EBP.SOVIET.M1910_MAXIM_HEAVY_MACHINE_GUN","EBP.SOVIET.M1910_MAXIM_HEAVY_MACHINE_GUN_MP","EBP.SOVIET.M1931_203MM_B_4_HOWITZER_ARTILLERY","EBP.SOVIET.M1931_203MM_B_4_HOWITZER_ARTILLERY_COMMANDER_MP","EBP.SOVIET.M1931_203MM_B_4_HOWITZER_ARTILLERY_MP","EBP.SOVIET.M1937_152MM_ML_20_ARTILLERY","EBP.SOVIET.M1937_152MM_ML_20_ARTILLERY_MP","EBP.SOVIET.M1937_53_K_45MM_AT_GUN","EBP.SOVIET.M1937_53_K_45MM_AT_GUN_MP","EBP.SOVIET.M1942_76MM_DIVISIONAL_GUN_ZIS_3","EBP.SOVIET.M1942_76MM_DIVISIONAL_GUN_ZIS_3_MP","EBP.SOVIET.M3A1_SCOUT_CAR","EBP.SOVIET.M3A1_SCOUT_CAR_MP","EBP.SOVIET.M5_HALFTRACK","EBP.SOVIET.M5_HALFTRACK_ASSAULT_MP","EBP.SOVIET.M5_HALFTRACK_MP","EBP.SOVIET.MACHINE_GUN_NEST","EBP.SOVIET.MACHINE_GUN_NEST_MP","EBP.SOVIET.MAXIM_WEAPON_CREW","EBP.SOVIET.MAXIM_WEAPON_CREW_MP","EBP.SOVIET.MEDIC","EBP.SOVIET.MEDIC_MP","EBP.SOVIET.MORTAR_120MM_WEAPON_CREW_MP","EBP.SOVIET.MORTAR_WEAPON_CREW","EBP.SOVIET.MORTAR_WEAPON_CREW_MP","EBP.SOVIET.MOTORPOOL","EBP.SOVIET.MOTORPOOL_MP","EBP.SOVIET.OBSERVATION_POST_FUEL","EBP.SOVIET.OBSERVATION_POST_FUEL_MP","EBP.SOVIET.OBSERVATION_POST_MUNITION","EBP.SOVIET.OBSERVATION_POST_MUNITION_MP","EBP.SOVIET.PARTISAN_SNIPER","EBP.SOVIET.PARTISAN_TROOP_KAR98K","EBP.SOVIET.PARTISAN_TROOP_KAR98K_2","EBP.SOVIET.PARTISAN_TROOP_KAR98K_2_MP","EBP.SOVIET.PARTISAN_TROOP_KAR98K_MP","EBP.SOVIET.PARTISAN_TROOP_KAR98K_TOW_BD","EBP.SOVIET.PARTISAN_TROOP_KAR98K_TOW_MP","EBP.SOVIET.PARTISAN_TROOP_NAGANT","EBP.SOVIET.PARTISAN_TROOP_NAGANT_MP","EBP.SOVIET.PARTISAN_TROOP_NAGANT_TOW_MP","EBP.SOVIET.PARTISAN_TROOPS_ANTITANK","EBP.SOVIET.PARTISAN_TROOPS_LMG","EBP.SOVIET.PARTISAN_TROOPS_RIFLE","EBP.SOVIET.PARTISAN_TROOPS_SMG","EBP.SOVIET.PENAL_BATTALION_TROOPS","EBP.SOVIET.PENAL_BATTALION_TROOPS_MP","EBP.SOVIET.PM41_82MM_MORTAR","EBP.SOVIET.PM41_82MM_MORTAR_MP","EBP.SOVIET.REFUGEE_FEMALE","EBP.SOVIET.REFUGEE_FEMALE_MP","EBP.SOVIET.REFUGEE_MALE","EBP.SOVIET.REFUGEE_MALE_MP","EBP.SOVIET.REPAIR_ENGINEER","EBP.SOVIET.REPAIR_ENGINEER_MP","EBP.SOVIET.REPAIR_STATION_MP","EBP.SOVIET.SAND_BAG_SOVIET","EBP.SOVIET.SAND_BAG_SOVIET_MP","EBP.SOVIET.SAND_BAG_SOVIET_TUTORIAL","EBP.SOVIET.SHERMAN_SOVIET","EBP.SOVIET.SHOCK_TROOPS","EBP.SOVIET.SHOCK_TROOPS_MP","EBP.SOVIET.SNIPER","EBP.SOVIET.SNIPER_ATK_TARGET","EBP.SOVIET.SNIPER_MP","EBP.SOVIET.SNIPER_RECON","EBP.SOVIET.SNIPER_RECON_MP","EBP.SOVIET.SOVIET_ALLIED_CARGO_PLANE","EBP.SOVIET.SOVIET_BASE_STAMPER","EBP.SOVIET.SOVIET_MINE","EBP.SOVIET.SOVIET_MINE_M08","EBP.SOVIET.SOVIET_MINE_MP","EBP.SOVIET.SOVIET_MINE_SP","EBP.SOVIET.SOVIET_MINE_TOW","EBP.SOVIET.SOVIET_OFFICER","EBP.SOVIET.SOVIET_OFFICER_MP","EBP.SOVIET.STEAM_TRAIN","EBP.SOVIET.SU_76M","EBP.SOVIET.SU_76M_MP","EBP.SOVIET.SU_85","EBP.SOVIET.SU_85_MP","EBP.SOVIET.T_34_76","EBP.SOVIET.T_34_76_MP","EBP.SOVIET.T_34_85","EBP.SOVIET.T_34_85_MP","EBP.SOVIET.T_70M","EBP.SOVIET.T_70M_MP","EBP.SOVIET.TANK_DEPOT","EBP.SOVIET.TANK_DEPOT_MP","EBP.SOVIET.TANKTRAP","EBP.SOVIET.TOW_COLD_WEAETHER_GUARD_TROOPS","EBP.SOVIET.US6_TRUCK","EBP.SOVIET.US6_TRUCK_MP","EBP.SOVIET.WEAPON_SUPPORT_CENTER","EBP.SOVIET.WEAPON_SUPPORT_CENTER_MP","EBP.SOVIET.WIRE_FIELD","EBP.SOVIET.WIRE_FIELD_MP","EBP.SOVIET.ZIS_6_TRANSPORT","EBP.SOVIET.ZIS_6_TRANSPORT_MP","SBP.SOVIET.BASE_CONSCRIPT_SQUAD","SBP.SOVIET.BASE_CONSCRIPT_SQUAD_MP","SBP.SOVIET.BOAT_01","SBP.SOVIET.CARGO_PLANE_SOVIET","SBP.SOVIET.COMBAT_ENGINEER_SQUAD","SBP.SOVIET.COMBAT_ENGINEER_SQUAD_MP","SBP.SOVIET.COMMISSAR_227","SBP.SOVIET.COMMISSAR_SQUAD_BATTLE","SBP.SOVIET.COMMISSAR_SQUAD_MP","SBP.SOVIET.COMMISSAR_SQUAD_TOW","SBP.SOVIET.CONSCRIPT_SQUAD","SBP.SOVIET.CONSCRIPT_SQUAD_MP","SBP.SOVIET.CONSCRIPT_SQUAD_TUTORIAL","SBP.SOVIET.DSHK_38_HMG_SQUAD","SBP.SOVIET.DSHK_38_HMG_SQUAD_MP","SBP.SOVIET.GUARDS_TROOPS","SBP.SOVIET.GUARDS_TROOPS_ASSAULT_MP","SBP.SOVIET.GUARDS_TROOPS_M08","SBP.SOVIET.GUARDS_TROOPS_MP","SBP.SOVIET.HM_120_38_MORTAR_SQUAD","SBP.SOVIET.HM_120_38_MORTAR_SQUAD_MP","SBP.SOVIET.IL_2_STUMOVIK_SQUAD","SBP.SOVIET.IL_2_STUMOVIK_SQUAD_ADVANCED_MP","SBP.SOVIET.IL_2_STUMOVIK_SQUAD_MP","SBP.SOVIET.IL_2_STURMOVIK_ANTI_TANK_BOMB_SQUAD_MP","SBP.SOVIET.IL_2_STURMOVIK_MARK_VEHICLE_SQUAD_MP","SBP.SOVIET.IL_2_STURMOVIK_RECON_SQUAD","SBP.SOVIET.IL_2_STURMOVIK_RECON_SQUAD_MP","SBP.SOVIET.IL_2_STURMOVIK_RECON_SQUAD_SP","SBP.SOVIET.IL_2_STURMOVIK_ROCKET_SP_SQUAD","SBP.SOVIET.IL_2_STURMOVIK_ROCKET_SP_SQUAD_MP","SBP.SOVIET.IL_2_STURMOVIK_ROCKET_SQUAD","SBP.SOVIET.IL_2_STURMOVIK_ROCKET_SQUAD_MP","SBP.SOVIET.IS_2","SBP.SOVIET.IS_2_MP","SBP.SOVIET.IS_2_TOW","SBP.SOVIET.ISU_152","SBP.SOVIET.ISU_152_MP","SBP.SOVIET.KATYUSHA_BM_13N_SQUAD","SBP.SOVIET.KATYUSHA_BM_13N_SQUAD_MP","SBP.SOVIET.KV_1","SBP.SOVIET.KV_1_COMMANDER_MP","SBP.SOVIET.KV_1_MP","SBP.SOVIET.KV_1_SP","SBP.SOVIET.KV_2","SBP.SOVIET.KV_2_MP","SBP.SOVIET.KV_2_TOW","SBP.SOVIET.KV_2_TOW_BATTLE","SBP.SOVIET.KV_8","SBP.SOVIET.KV_8_MP","SBP.SOVIET.M01_CONSCRIPT_SQUAD_DOCKS","SBP.SOVIET.M01_CONSCRIPT_SQUAD_HARMLESS","SBP.SOVIET.M01_CONSCRIPT_SQUAD_HARMLESS_DURABLE","SBP.SOVIET.M01_CONSCRIPT_SQUAD_WOUNDED","SBP.SOVIET.M01_IL_2_STURMOVIK_ROCKET_SQUAD","SBP.SOVIET.M01_IL2_DOGFIGHT","SBP.SOVIET.M01_MEDIC","SBP.SOVIET.M02_COMBAT_ENGINEER_SQUAD","SBP.SOVIET.M02_REFUGEE_SQUAD","SBP.SOVIET.M08_COMBAT_ENGINEER_SQUAD","SBP.SOVIET.M08_T_34_76_SQUAD_SMALLPATH","SBP.SOVIET.M08_TANK_BUSTER_CONSCRIPT_SQUAD","SBP.SOVIET.M11_ANIA_SNIPER_SQUAD","SBP.SOVIET.M11_ISAKOVICH_SQUAD","SBP.SOVIET.M11_PARTISAN_SQUAD_KAR98K_RIFLE","SBP.SOVIET.M11_PARTISAN_SQUAD_NAGANT_RIFLE","SBP.SOVIET.M11_PARTISAN_SQUAD_NOWEAPON","SBP.SOVIET.M11_SNIPER_TEAM","SBP.SOVIET.M1910_MAXIM_HEAVY_MACHINE_GUN_SQUAD","SBP.SOVIET.M1910_MAXIM_HEAVY_MACHINE_GUN_SQUAD_MP","SBP.SOVIET.M1931_203MM_B_4_HOWITZER_ARTILLERY","SBP.SOVIET.M1931_203MM_B_4_HOWITZER_ARTILLERY_COMMANDER_MP","SBP.SOVIET.M1931_203MM_B_4_HOWITZER_ARTILLERY_MP","SBP.SOVIET.M1937_152MM_ML_20_ARTILLERY","SBP.SOVIET.M1937_152MM_ML_20_ARTILLERY_MP","SBP.SOVIET.M1937_53_K_45MM_AT_GUN_SQUAD","SBP.SOVIET.M1937_53_K_45MM_AT_GUN_SQUAD_MP","SBP.SOVIET.M1942_ZIS_3_76MM_AT_GUN_SQUAD","SBP.SOVIET.M1942_ZIS_3_76MM_AT_GUN_SQUAD_MP","SBP.SOVIET.M3A1_SCOUT_CAR_SQUAD","SBP.SOVIET.M3A1_SCOUT_CAR_SQUAD_MP","SBP.SOVIET.M5_HALFTRACK__ASSAULT_SQUAD_MP","SBP.SOVIET.M5_HALFTRACK_SQUAD","SBP.SOVIET.M5_HALFTRACK_SQUAD_MP","SBP.SOVIET.PARTISAN_SQUAD_GRANATEWERFER_34_81MM_MORTAR","SBP.SOVIET.PARTISAN_SQUAD_GRANATEWERFER_34_81MM_MORTAR_MP","SBP.SOVIET.PARTISAN_SQUAD_KAR98K_RIFLE","SBP.SOVIET.PARTISAN_SQUAD_KAR98K_RIFLE_MP","SBP.SOVIET.PARTISAN_SQUAD_MAXIM_HMG","SBP.SOVIET.PARTISAN_SQUAD_MAXIM_HMG_MP","SBP.SOVIET.PARTISAN_SQUAD_MG42_HMG","SBP.SOVIET.PARTISAN_SQUAD_MG42_HMG_MP","SBP.SOVIET.PARTISAN_SQUAD_NAGANT_RIFLE","SBP.SOVIET.PARTISAN_SQUAD_NAGANT_RIFLE_MP","SBP.SOVIET.PARTISAN_SQUAD_PM_82_41_MORTAR","SBP.SOVIET.PARTISAN_SQUAD_PM_82_41_MORTAR_MP","SBP.SOVIET.PARTISANS_LMG_MP","SBP.SOVIET.PARTISANS_PANZERSCHRECK_MP","SBP.SOVIET.PARTISANS_PTRS_MP","SBP.SOVIET.PARTISANS_RIFLE_MP","SBP.SOVIET.PARTISANS_SMG_MP","SBP.SOVIET.PENAL_BATTALION","SBP.SOVIET.PENAL_BATTALION_MP","SBP.SOVIET.PM_82_41_MORTAR_SQUAD","SBP.SOVIET.PM_82_41_MORTAR_SQUAD_MP","SBP.SOVIET.SHOCK_TROOPS","SBP.SOVIET.SHOCK_TROOPS_M11","SBP.SOVIET.SHOCK_TROOPS_MP","SBP.SOVIET.SNIPER_TEAM","SBP.SOVIET.SNIPER_TEAM_MALE","SBP.SOVIET.SNIPER_TEAM_MP","SBP.SOVIET.SOVIET_76MM_SHERMAN_MP","SBP.SOVIET.SOVIET_ALLIED_CARGO_PLANE","SBP.SOVIET.SOVIET_OFFICER_SQUAD","SBP.SOVIET.SOVIET_OFFICER_SQUAD_MP","SBP.SOVIET.STEAM_TRAIN","SBP.SOVIET.SU_76M","SBP.SOVIET.SU_76M_MP","SBP.SOVIET.SU_76M_TOW","SBP.SOVIET.SU_85","SBP.SOVIET.SU_85_MP","SBP.SOVIET.T_34_76_SQUAD","SBP.SOVIET.T_34_76_SQUAD_MP","SBP.SOVIET.T_34_85_ADVANCED_SQUAD_MP","SBP.SOVIET.T_34_85_SQUAD","SBP.SOVIET.T_34_85_SQUAD_MP","SBP.SOVIET.T_70M","SBP.SOVIET.T_70M_MP","SBP.SOVIET.TOW_BRIDGE_PARTISAN_SQUAD_AT","SBP.SOVIET.TOW_BRIDGE_PARTISAN_SQUAD_BASE","SBP.SOVIET.TOW_BRIDGE_PARTISAN_SQUAD_MAXIM","SBP.SOVIET.TOW_BRIDGE_PARTISAN_SQUAD_MORTAR","SBP.SOVIET.TOW_COLD_WEATHER_GUARDS_TROOPS","SBP.SOVIET.TOW_PARTISAN_SQUAD_KAR98K_RIFLE_MP","SBP.SOVIET.TOW_PARTISAN_SQUAD_LMG_SQUAD","SBP.SOVIET.TOW_PARTISAN_SQUAD_MAXIM_HMG_MP","SBP.SOVIET.US6_TRUCK_SQUAD","SBP.SOVIET.ZIS_6_TRANSPORT_TRUCK","SBP.SOVIET.ZIS_6_TRANSPORT_TRUCK_MP","ABILITY.SOVIET.ALLIED_AIR_SUPPLIES","ABILITY.SOVIET.ANTI_PERSONNEL_MINES","ABILITY.SOVIET.ANTI_TANK_GRENADE","ABILITY.SOVIET.ANTI_TANK_GRENADE_ASSAULT","ABILITY.SOVIET.ANTI_TANK_GRENADE_MP","ABILITY.SOVIET.ANTI_TANK_GRENADE_NO_REQUIREMENTS_MP","ABILITY.SOVIET.AT_76MM_HE_BARRAGE_ABILITY","ABILITY.SOVIET.AT_76MM_HE_BARRAGE_ABILITY_MP","ABILITY.SOVIET.AT_GUN_AMBUSH_TACTICS","ABILITY.SOVIET.B4_203MM_BARRAGE","ABILITY.SOVIET.B4_203MM_BARRAGE_COMMANDER_MP","ABILITY.SOVIET.B4_203MM_BARRAGE_COMMANDER_PRECISE_MP","ABILITY.SOVIET.B4_203MM_BARRAGE_COMMANDER_VET3_MP","ABILITY.SOVIET.B4_203MM_BARRAGE_COMMANDER_VICTORTARGET_MP","ABILITY.SOVIET.B4_203MM_BARRAGE_MP","ABILITY.SOVIET.B4_203MM_DIRECT_FIRE","ABILITY.SOVIET.B4_203MM_HOWITZER","ABILITY.SOVIET.BASE_CONSCRIPT_DISPATCH","ABILITY.SOVIET.BASE_CONSCRIPT_DISPATCH_MP","ABILITY.SOVIET.BOMBARDMENT_FX","ABILITY.SOVIET.BOOBY_TRAP","ABILITY.SOVIET.BUTTON_VEHICLE","ABILITY.SOVIET.BUTTON_VEHICLE_MP","ABILITY.SOVIET.BUTTON_VEHICLE_TOW","ABILITY.SOVIET.CAMPAIGN_SHOCK_FIRE_SUPERIORITY","ABILITY.SOVIET.CMD_120MM_MORTAR_CREW","ABILITY.SOVIET.CMD_ADVANCED_T34_85_MEDIUM_TANK","ABILITY.SOVIET.CMD_AT_GUN_AMBUSH_TACTICS_MP","ABILITY.SOVIET.CMD_CONSCRIPT_ASSAULT_PACKAGE","ABILITY.SOVIET.CMD_CONSCRIPT_EVASIVE_TACTICS","ABILITY.SOVIET.CMD_CONSCRIPT_REPAIR_KIT","ABILITY.SOVIET.CMD_GUARD_TROOPS","ABILITY.SOVIET.CMD_IS2_HEAVY_TANK","ABILITY.SOVIET.CMD_ISU_152","ABILITY.SOVIET.CMD_KATYUSHA","ABILITY.SOVIET.CMD_KV_1_UNLOCK","ABILITY.SOVIET.CMD_KV_8_UNLOCK_MP","ABILITY.SOVIET.CMD_ML_20","ABILITY.SOVIET.CMD_PENAL_BATTALION","ABILITY.SOVIET.CMD_RADIO_INTERCEPT","ABILITY.SOVIET.CMD_SHOCK_TROOPS","ABILITY.SOVIET.CMD_SOVIET_INDUSTRY","ABILITY.SOVIET.CMD_T34_85_MEDIUM_TANK","ABILITY.SOVIET.CMD_VEHICLE_CREW_REPAIR_TRAINING","ABILITY.SOVIET.COMMISSAR_SQUAD_MP","ABILITY.SOVIET.CONE_LOS_TOGGLE_ABILITY","ABILITY.SOVIET.CONE_LOS_TOGGLE_ABILITY_MP","ABILITY.SOVIET.CONSCRIPT_ANTI_TANK_GRENADE_ASSAULT_MP","ABILITY.SOVIET.CONSCRIPT_DISPATCH_MP","ABILITY.SOVIET.CONSCRIPT_EVASIVE_TACTICS","ABILITY.SOVIET.CONSCRIPT_EVASIVE_TACTICS_MP","ABILITY.SOVIET.CONSCRIPT_MOLOTOV_COCKTAIL","ABILITY.SOVIET.CONSCRIPT_MOLOTOV_COCKTAIL_MP","ABILITY.SOVIET.CONSCRIPT_OORAH","ABILITY.SOVIET.CONSCRIPT_OORAH_MP","ABILITY.SOVIET.CONSCRIPT_PTRS_UPGRADE","ABILITY.SOVIET.DSHK_ARMOR_PIERCING","ABILITY.SOVIET.DSHK_MP","ABILITY.SOVIET.ENGINEER_SALVAGE_WRECK","ABILITY.SOVIET.FATALITY_FEAR_PROPAGANDA_ARTILLERY","ABILITY.SOVIET.FATALITY_INCENDIARY_ARTILLERY","ABILITY.SOVIET.FATALITY_KATYUSHA_ROCKETS","ABILITY.SOVIET.FEAR_PROPAGANDA_ARTILLERY","ABILITY.SOVIET.FIELDCRAFT_TRIP_FLARE","ABILITY.SOVIET.FIELDCRAFT_TRIP_FLARE_MP","ABILITY.SOVIET.FIRE_ARTILLERY","ABILITY.SOVIET.FOR_MOTHER_RUSSIA_ABILITY","ABILITY.SOVIET.FORWARD_HQ","ABILITY.SOVIET.FRONTOVIKI_CONSCRIPT_DISPATCH","ABILITY.SOVIET.GUARDS_THROW_DEFENSIVE_GRENADE","ABILITY.SOVIET.GUARDS_THROW_DEFENSIVE_GRENADE_MP","ABILITY.SOVIET.HOLD_THE_LINE","ABILITY.SOVIET.IL_2_ANTI_TANK_BOMB_STRIKE","ABILITY.SOVIET.IL_2_ATTACK_STRAFE","ABILITY.SOVIET.IL_2_BOMBING_RUN_SP","ABILITY.SOVIET.IL_2_PRECISION_BOMB_STRIKE","ABILITY.SOVIET.IL_2_RECON","ABILITY.SOVIET.IL_2_RECON_SINGLEPASS_SP","ABILITY.SOVIET.IL_2_RECON_SP","ABILITY.SOVIET.IL_2_STURMOVIK_ATTACK","ABILITY.SOVIET.IL_2_STURMOVIK_ATTACK_ADVANCED","ABILITY.SOVIET.IL_2_SUPPORT","ABILITY.SOVIET.IL_2_SUPPORT_PRECISION_SP","ABILITY.SOVIET.IL_2_SUPPORT_SP","ABILITY.SOVIET.IS2_DISPATCH_SP","ABILITY.SOVIET.IS2_TANK_DEFENSIVE_WEAPON_MP","ABILITY.SOVIET.ISU_152_DISPATCH_SP","ABILITY.SOVIET.ISU_152_PIERCING_SHOT_ABILITY","ABILITY.SOVIET.ISU_152_PIERCING_SHOT_ABILITY_MP","ABILITY.SOVIET.ISU152_AMMO_SWITCH_AP_SHELL_MP","ABILITY.SOVIET.ISU152_AMMO_SWITCH_HE_SHELL_MP","ABILITY.SOVIET.ISU152_CONCRETE_PIERCING_ROUND_MP","ABILITY.SOVIET.KATUSHYA_CREEPING_BARRAGE_MP","ABILITY.SOVIET.KAYTUSHA_ROCKET_TRUCK_BARRAGE","ABILITY.SOVIET.KAYTUSHA_ROCKET_TRUCK_BARRAGE_MP","ABILITY.SOVIET.KAYTUSHA_ROCKET_TRUCK_BARRAGE_VET3_MP","ABILITY.SOVIET.KAYTUSHA_ROCKET_TRUCK_BARRAGE_VICTORTARGET_MP","ABILITY.SOVIET.KAYTUSHA_ROCKET_TRUCK_CREEPING_BARRAGE_MP","ABILITY.SOVIET.KAYTUSHA_ROCKET_TRUCK_PRECISION_BARRAGE","ABILITY.SOVIET.KAYTUSHA_ROCKET_TRUCK_PRECISION_BARRAGE_MP","ABILITY.SOVIET.KV_2","ABILITY.SOVIET.KV_2_SEIGE_MODE","ABILITY.SOVIET.KV_8_FLAME_45MM_TOGGLE_MP","ABILITY.SOVIET.LIGHT_ANTI_VEHICLE_MINES","ABILITY.SOVIET.M_42_AT_GUN","ABILITY.SOVIET.M11_PARTISANS_DISPATCH_KARK98K","ABILITY.SOVIET.M11_PARTISANS_DISPATCH_NAGANT","ABILITY.SOVIET.M11_SNIPER_DISPATCH02","ABILITY.SOVIET.M11_SNIPER_DISPATCH02_MP","ABILITY.SOVIET.M11_SNIPER_HOLD_FIRE","ABILITY.SOVIET.M3A1_M5_MOVING_ACCURACY_MP","ABILITY.SOVIET.M5_HALFTRACK_ASSAULT","ABILITY.SOVIET.M5_M3A1_OVERDRIVE","ABILITY.SOVIET.M5_M3A1_OVERDRIVE_MP","ABILITY.SOVIET.MANPOWER_BLITZ","ABILITY.SOVIET.MARK_VEHICLE","ABILITY.SOVIET.MAXIM_HMG_DISPATCH_SP","ABILITY.SOVIET.MERGE_ABILITY","ABILITY.SOVIET.MERGE_ABILITY_MP","ABILITY.SOVIET.ML_20_152MM_BARRAGE_ABILITY","ABILITY.SOVIET.ML_20_152MM_BARRAGE_ABILITY_MP","ABILITY.SOVIET.ML_20_152MM_BARRAGE_ABILITY_SLOW","ABILITY.SOVIET.ML_20_152MM_BARRAGE_ABILITY_SLOW_MP","ABILITY.SOVIET.ML_20_152MM_BARRAGE_ABILITY_VET_1_MP","ABILITY.SOVIET.ML_20_152MM_BARRAGE_ABILITY_VET3_MP","ABILITY.SOVIET.ML_20_152MM_BARRAGE_ABILITY_VICTORTARGET_MP","ABILITY.SOVIET.ML_20_152MM_BARRAGE_PRECISON_ABILITY_MP","ABILITY.SOVIET.MORTAR_EXPLOSION_FX","ABILITY.SOVIET.MORTAR_EXPLOSION_FX_ICE","ABILITY.SOVIET.MORTAR_FIRE_FLARES_ABILITY_MP","ABILITY.SOVIET.MORTAR_PRECISION_BARRAGE_120MM_VET","ABILITY.SOVIET.MORTAR_PRECISION_BARRAGE_120MM_VET_MP","ABILITY.SOVIET.MORTAR_PRECISION_BARRAGE_82MM","ABILITY.SOVIET.MORTAR_PRECISION_BARRAGE_82MM_MP","ABILITY.SOVIET.NO_RETREAT_NO_SURRENDER","ABILITY.SOVIET.PARTISAN_DISPATCH","ABILITY.SOVIET.PARTISAN_DISPATCH_TOW","ABILITY.SOVIET.PARTISAN_MOLOTOV_COCKTAIL_MP","ABILITY.SOVIET.PARTISANS_COMMANDER_ANTI_INFANTRY","ABILITY.SOVIET.PARTISANS_COMMANDER_ANTI_VEHICLE","ABILITY.SOVIET.PENAL_OORAH_MP","ABILITY.SOVIET.PENAL_TROOP_DISPATCH_SINGLE_SP","ABILITY.SOVIET.PENAL_TROOP_DISPATCH_SP","ABILITY.SOVIET.RAPID_CONSCRIPTION","ABILITY.SOVIET.REPAIR_STATION","ABILITY.SOVIET.RG_42_ANTI_PERSONNEL_GRENADE","ABILITY.SOVIET.RG_42_ANTI_PERSONNEL_GRENADE_MP","ABILITY.SOVIET.RGD_1_SMOKE_GRENADE","ABILITY.SOVIET.RGD_1_SMOKE_GRENADE_MP","ABILITY.SOVIET.RGD_33_PARTISAN_GRENADE_MP","ABILITY.SOVIET.SALVAGE_KITS","ABILITY.SOVIET.SATCHEL_CHARGE_THROW_ABILITY_MP","ABILITY.SOVIET.SCORCHED_EARTH_POLICY","ABILITY.SOVIET.SCORCHED_EARTH_POLICY_MP","ABILITY.SOVIET.SHERMAN_SOVIET_DISPATCH","ABILITY.SOVIET.SHERMAN76MM_AMMO_SWITCH_AP_SHELL_MP","ABILITY.SOVIET.SHERMAN76MM_AMMO_SWITCH_HE_SHELL_MP","ABILITY.SOVIET.SHOCK_TROOP_DISPATCH_SP","ABILITY.SOVIET.SHOCK_TROOP_SMOKE_GRENADES","ABILITY.SOVIET.SMOKE_120MM_MORTAR_BARRAGE","ABILITY.SOVIET.SMOKE_120MM_MORTAR_BARRAGE_MP","ABILITY.SOVIET.SMOKE_SYNC_MORTAR_BARRAGE","ABILITY.SOVIET.SMOKE_SYNC_MORTAR_BARRAGE_MP","ABILITY.SOVIET.SNIPER_DELAYED_COVER_AUTO_CAMOUFLAGE","ABILITY.SOVIET.SNIPER_DELAYED_COVER_AUTO_CAMOUFLAGE_MP","ABILITY.SOVIET.SNIPER_FIRE_FLARES_ABILITY","ABILITY.SOVIET.SNIPER_FIRE_FLARES_ABILITY_MP","ABILITY.SOVIET.SNIPER_HMG_SPRINT","ABILITY.SOVIET.SNIPER_HMG_SPRINT_MP","ABILITY.SOVIET.SNIPER_HOLD_FIRE","ABILITY.SOVIET.SNIPER_HOLD_FIRE_MP","ABILITY.SOVIET.SNIPER_IN_COVER_AUTO_CAMOUFLAGE","ABILITY.SOVIET.SNIPER_IN_COVER_AUTO_CAMOUFLAGE_MP","ABILITY.SOVIET.SNIPER_SUPPRESSION_FIRE_ABILITY","ABILITY.SOVIET.SNIPER_SUPPRESSION_FIRE_ABILITY_MP","ABILITY.SOVIET.SOV_VEHICLE_HOLD_FIRE_MP","ABILITY.SOVIET.SOVIET_BARBED_WIRE_CUTTING_ABILITY","ABILITY.SOVIET.SOVIET_BARBED_WIRE_CUTTING_ABILITY_MP","ABILITY.SOVIET.SOVIET_CAMO_HOLD_FIRE_MP","ABILITY.SOVIET.SOVIET_CONSCRIPT_REPAIR_ABILITY","ABILITY.SOVIET.SOVIET_CONSCRIPT_REPAIR_ABILITY_MP","ABILITY.SOVIET.SOVIET_HQ_ENGINEER_CALL_IN","ABILITY.SOVIET.SOVIET_INDUSTRY","ABILITY.SOVIET.SOVIET_REPAIR_ABILITY","ABILITY.SOVIET.SOVIET_REPAIR_ABILITY_MP","ABILITY.SOVIET.SOVIET_WAR_MACHINE_SP","ABILITY.SOVIET.SPY_NETWORK","ABILITY.SOVIET.SU_76_BARRAGE_ABILITY","ABILITY.SOVIET.SU_76_BARRAGE_ABILITY_MP","ABILITY.SOVIET.SU76_SU85_ZIS3_53K_ISU152_INFANTRY_TRACKING","ABILITY.SOVIET.SU76_SU85_ZIS3_53K_ISU152_INFANTRY_TRACKING_MP","ABILITY.SOVIET.SYNC_MORTAR_BARRAGE","ABILITY.SOVIET.SYNC_MORTAR_BARRAGE_120MM","ABILITY.SOVIET.SYNC_MORTAR_BARRAGE_120MM_MP","ABILITY.SOVIET.SYNC_MORTAR_BARRAGE_120MM_VICTORTARGET_MP","ABILITY.SOVIET.SYNC_MORTAR_BARRAGE_MP","ABILITY.SOVIET.SYNC_MORTAR_BARRAGE_VICTORTARGET_MP","ABILITY.SOVIET.T_34_RAMMING_ABILITY","ABILITY.SOVIET.T_34_RAMMING_ABILITY_MP","ABILITY.SOVIET.T70_CREW_REPAIR_ABILITY","ABILITY.SOVIET.T70_CREW_REPAIR_ABILITY_MP","ABILITY.SOVIET.TANK_DETECTION_ABILITY","ABILITY.SOVIET.TANK_TRAPS","ABILITY.SOVIET.TANK_VET_POINT_CAPTURE_ABILITY","ABILITY.SOVIET.TANK_VET_POINT_CAPTURE_ABILITY_MP","ABILITY.SOVIET.TO_THE_LAST_MAN_MP","ABILITY.SOVIET.VEHICLE_CREW_REPAIR_ABILITY","ABILITY.SOVIET.VEHICLE_CREW_REPAIR_ABILITY_MP","ABILITY.SOVIET.VEHICLE_CREW_REPAIR_TOGGLE_MP","ABILITY.SOVIET.VEHICLE_RECON_TOGGLE","ABILITY.SOVIET.VEHICLE_RECON_TOGGLE_MP","ABILITY.SOVIET.VEHICLE_RECON_TOGGLE_VET2_MP","UPG.SOVIET.ABILITY_LOCK_OUT_CONSCRIPT","UPG.SOVIET.ABILITY_LOCK_OUT_CONSCRIPT_MP","UPG.SOVIET.ALLIED_AIR_SUPPLIES","UPG.SOVIET.ANTI_PERSONNEL_MINES","UPG.SOVIET.ANTI_TANK_GUN_AMBUSH_TACTICS","UPG.SOVIET.BASE_CONSCRIPT_AT_GRENADE_UNLOCK","UPG.SOVIET.BASE_CONSCRIPT_AT_GRENADE_UNLOCK_MP","UPG.SOVIET.BASE_CONSCRIPT_MOLOTOV_UNLOCK","UPG.SOVIET.BASE_CONSCRIPT_MOLOTOV_UNLOCK_MP","UPG.SOVIET.BASE_CONSCRIPT_OORAH_UNLOCK","UPG.SOVIET.BASE_CONSCRIPT_OORAH_UNLOCK_MP","UPG.SOVIET.BASE_CONSCRIPT_REPAIR_UNLOCK_MP","UPG.SOVIET.BASE_CONSCRIPT_RIFLE_UNLOCK_MP","UPG.SOVIET.BOOBY_TRAP","UPG.SOVIET.CAMOUFLAGE_NET_ACTIVATED_SOVIET","UPG.SOVIET.COMMANDER_T34_85_MP","UPG.SOVIET.COMMISSAR_SQUAD","UPG.SOVIET.CONSCRIPT_ASSAULT_PACKAGE","UPG.SOVIET.CONSCRIPT_ASSAULT_PACKAGE_INGAME","UPG.SOVIET.CONSCRIPT_AT_GRENADE_ASSAULT","UPG.SOVIET.CONSCRIPT_DP_28_LMG_PACKAGE","UPG.SOVIET.CONSCRIPT_EVASIVE_TACTICS","UPG.SOVIET.CONSCRIPT_MOBILIZE_UNLOCK","UPG.SOVIET.CONSCRIPT_PTRS","UPG.SOVIET.CONSCRIPT_PTRS_PACKAGE","UPG.SOVIET.CONSCRIPT_REPAIR_KIT","UPG.SOVIET.DEMO_IL_2_STRAFING_RUN","UPG.SOVIET.DSHK_MACHINEGUN","UPG.SOVIET.ENGINEER_FLAMETHROWER","UPG.SOVIET.ENGINEER_FLAMETHROWER_MP","UPG.SOVIET.ENGINEER_MINESWEEPER","UPG.SOVIET.ENGINEER_MINESWEEPER_MP","UPG.SOVIET.ENGINEER_SALVAGE_KIT","UPG.SOVIET.ENGINEER_SALVAGE_KITS_UNLOCK","UPG.SOVIET.EVASIVE_TACTICS_IS_ON","UPG.SOVIET.FEAR_PROPAGANDA","UPG.SOVIET.FIRE_ARTILLERY","UPG.SOVIET.FOR_MOTHER_RUSSIA","UPG.SOVIET.FORWARD_HQ","UPG.SOVIET.FORWARD_HQ_AURA","UPG.SOVIET.GUARD_ARCHETYPE","UPG.SOVIET.GUARD_DP_28_LMG_PACKAGE","UPG.SOVIET.GUARD_DP_28_LMG_PACKAGE_MP","UPG.SOVIET.GUARD_TROOPS","UPG.SOVIET.HM120_MORTAR_UNLOCK","UPG.SOVIET.HOLD_FIRE_SOVIET_CAMMO","UPG.SOVIET.HOLD_THE_LINE","UPG.SOVIET.HOWTIZER_203MM","UPG.SOVIET.HQ_ANTI_TANK_GRENADE","UPG.SOVIET.HQ_ANTI_TANK_GRENADE_MP","UPG.SOVIET.HQ_CONSCRIPT_REPAIR_KIT","UPG.SOVIET.HQ_HEALING_AURA","UPG.SOVIET.HQ_HEALING_AURA_M13","UPG.SOVIET.HQ_HEALING_AURA_MP","UPG.SOVIET.HQ_MOLOTOV_GRENADE_MP","UPG.SOVIET.IL_2_ANTI_TANK_BOMB","UPG.SOVIET.IL_2_BOMB_STRIKE","UPG.SOVIET.IL_2_RECON","UPG.SOVIET.IL_2_STURMOVIK_ATTACK","UPG.SOVIET.IL_2_STURMOVIK_ATTACK_ADVANCED","UPG.SOVIET.IL_2_SUPPORT","UPG.SOVIET.IS_2_SUPPORT","UPG.SOVIET.IS2_TOP_GUNNER","UPG.SOVIET.IS2_TOP_GUNNER_MP","UPG.SOVIET.ISAKOVICH_A01","UPG.SOVIET.ISU152_HE_ROUNDS","UPG.SOVIET.ISU152_TOP_GUNNER","UPG.SOVIET.ISU152_TOP_GUNNER_MP","UPG.SOVIET.ISU152_UNLOCK","UPG.SOVIET.KATYUSHA_UNLOCK","UPG.SOVIET.KV_1_UNLOCK_DEMO","UPG.SOVIET.KV_8_UNLOCK","UPG.SOVIET.KV1_UNLOCK","UPG.SOVIET.KV2_UNLOCK","UPG.SOVIET.LIGHT_ANTI_VEHICLE_MINES","UPG.SOVIET.M_42_AT_GUN","UPG.SOVIET.M3_HALFTRACK_ASSAULT","UPG.SOVIET.M5_HALFTRACK_72K_AA_GUN_PACKAGE","UPG.SOVIET.M5_HALFTRACK_72K_AA_GUN_PACKAGE_MP","UPG.SOVIET.MANPOWER_BLITZ","UPG.SOVIET.MARK_VEHICLE","UPG.SOVIET.ML_20_HOWITZER_UNLOCK","UPG.SOVIET.NKVD_ARCHETYPE","UPG.SOVIET.ORDER_227_DISABLE","UPG.SOVIET.ORDER_227_LOCKDOWN","UPG.SOVIET.ORDER227","UPG.SOVIET.PARTISAN_COMMANDER_ANTIVEHICLE_TROOPS","UPG.SOVIET.PARTISAN_COMMANDER_TROOPS","UPG.SOVIET.PARTISAN_HEALTH_UPGRADE","UPG.SOVIET.PARTISAN_HEALTH_UPGRADE_TANK_HUNTER","UPG.SOVIET.PARTISAN_TROOPS","UPG.SOVIET.PARTISAN_TROOPS_TOW","UPG.SOVIET.PENAL_BATTALION","UPG.SOVIET.PENAL_BATTALION_FLAMETHROWER_PACKAGE","UPG.SOVIET.PENAL_BATTALION_FLAMETHROWER_PACKAGE_MP","UPG.SOVIET.PPSH_41_SUB_MACHINE_GUN_UPGRADE","UPG.SOVIET.PPSH_41_SUB_MACHINE_GUN_UPGRADE_MP","UPG.SOVIET.PTRS_41_AT_RIFLE_PACKAGE_GUARD_TROOP","UPG.SOVIET.PTRS_41_AT_RIFLE_PACKAGE_GUARD_TROOP_ASSAULT_MP","UPG.SOVIET.PTRS_41_AT_RIFLE_PACKAGE_GUARD_TROOP_BETTER_BALANCED","UPG.SOVIET.PTRS_41_AT_RIFLE_PACKAGE_GUARD_TROOP_MP","UPG.SOVIET.RADIO_INTERCEPT","UPG.SOVIET.RAPID_CONSCRIPTION","UPG.SOVIET.REPAIR_BUNKER","UPG.SOVIET.SCORCHED_EARTH_POLICY","UPG.SOVIET.SCORCHED_EARTH_POLICY_MP","UPG.SOVIET.SHERMAN_SOVIET_DISPATCH","UPG.SOVIET.SHERMAN_SOVIET_TOP_GUNNER","UPG.SOVIET.SHOCK_ARCHETYPE","UPG.SOVIET.SHOCK_TROOPS","UPG.SOVIET.SHOCK_TROOPS_SP","UPG.SOVIET.SOVIET_GRENADES_LONG_TIMER","UPG.SOVIET.SOVIET_INDUSTRY","UPG.SOVIET.SPY_NETWORK","UPG.SOVIET.T34_85_ADVANCED_UNLOCK","UPG.SOVIET.T34_85_UNLOCK","UPG.SOVIET.TANK_DETECTION","UPG.SOVIET.TANK_RAID_ENABLED","UPG.SOVIET.TANK_TRAPS","UPG.SOVIET.TOW_1941_SOVIET","UPG.SOVIET.VEHICLE_SELF_REPAIR_TRAINING","EBP.WEST_GERMAN.ANTI_TANK_GUN_CREW_MP","EBP.WEST_GERMAN.ARMORED_CAR_SDKFZ_223","EBP.WEST_GERMAN.ARTY_CREW_MP","EBP.WEST_GERMAN.ASSAULT_PIONEER_MP","EBP.WEST_GERMAN.ASSAULT_PIONEERS_HEAVY_MINE_MP","EBP.WEST_GERMAN.BASE_FLAK_GUN_MP","EBP.WEST_GERMAN.BASE_FLAK_SANDBAGS","EBP.WEST_GERMAN.BUNKER_WESTGERMAN_MP","EBP.WEST_GERMAN.FALLSCHIRMJAGER_MP","EBP.WEST_GERMAN.FIELD_OFFICER_MP","EBP.WEST_GERMAN.FLAK_EMPLACEMENT","EBP.WEST_GERMAN.FLAK_EMPLACEMENT_BASE","EBP.WEST_GERMAN.FLAK_EMPLACEMENT_CREW","EBP.WEST_GERMAN.FLAK_EMPLACEMENT_CREW_BASE","EBP.WEST_GERMAN.GOLIATH_MP","EBP.WEST_GERMAN.GRANATWERFER_34_81MM_MORTAR_WG_MP","EBP.WEST_GERMAN.HALFTRACK_SDKFZ_251_17_FLAK_MP","EBP.WEST_GERMAN.HALFTRACK_SDKFZ_251_20_IR_SEARCHLIGHT_MP","EBP.WEST_GERMAN.HALFTRACK_SDKFZ_251_20_IR_SEARCHLIGHT_SP","EBP.WEST_GERMAN.HALFTRACK_SDKFZ_251_MP_2","EBP.WEST_GERMAN.HALFTRACK_SDKFZ_251_WURFRAHMEN_40_MP","EBP.WEST_GERMAN.HEAVY_ARMOR_SUPPORT_MP","EBP.WEST_GERMAN.HEAVY_ARMOR_SUPPORT_PREPLACED","EBP.WEST_GERMAN.HETZER_MP","EBP.WEST_GERMAN.HMG_CREW_MP","EBP.WEST_GERMAN.HOWITZER_105MM_LE_FH18_MINICHALLENGE","EBP.WEST_GERMAN.HOWITZER_105MM_LONG_RANGE","EBP.WEST_GERMAN.INFANTRY_SUPPORT_MP","EBP.WEST_GERMAN.INFANTRY_SUPPORT_PREPLACED","EBP.WEST_GERMAN.JAEGER_LIGHT_INFANTRY_RECON","EBP.WEST_GERMAN.JAGDPANZER_IV_SDKFZ_162_MP","EBP.WEST_GERMAN.JAGDTIGER_SDKFZ_186_MP","EBP.WEST_GERMAN.JU52_PARATROOPER_PLANE","EBP.WEST_GERMAN.JU52_PLANE","EBP.WEST_GERMAN.KING_TIGER_SDKFZ_182_MP","EBP.WEST_GERMAN.KUBELWAGEN_TYPE_82_MP","EBP.WEST_GERMAN.LE_IG_18_INF_SUPPORT_GUN_MP","EBP.WEST_GERMAN.LIGHT_ARMOR_SUPPORT_MP","EBP.WEST_GERMAN.LIGHT_ARMOR_SUPPORT_PREPLACED","EBP.WEST_GERMAN.MED_SUPPLY_STASH","EBP.WEST_GERMAN.MG34_HMG_CREW","EBP.WEST_GERMAN.MG34_HMG_MP","EBP.WEST_GERMAN.MG42_HMG_WG_MP","EBP.WEST_GERMAN.MINE_FIELD_WESTGERMAN_MP","EBP.WEST_GERMAN.MORTAR_TEAM_CREW_MP","EBP.WEST_GERMAN.OBERSOLDATEN_MP","EBP.WEST_GERMAN.OKW_HOWITZER_105MM_LE_FH18_MP","EBP.WEST_GERMAN.OKW_HOWITZER_CREW_MP","EBP.WEST_GERMAN.OSTWIND_FLAK_PANZER_WEST_GERMAN_MP","EBP.WEST_GERMAN.PAK40_75MM_AT_GUN_WG_MP","EBP.WEST_GERMAN.PAK43_88MM_AT_GUN_WESTGERMAN_MP","EBP.WEST_GERMAN.PANTHER_SDKFZ_171_AUSF_G_MP","EBP.WEST_GERMAN.PANTHER_SDKFZ_171_COMMANDER_MP","EBP.WEST_GERMAN.PANZER_II_LUCHS_SDKFZ_123_MP","EBP.WEST_GERMAN.PANZER_IV_SDKFZ_AUSF_J_MP","EBP.WEST_GERMAN.PANZERFUSILIER_MP","EBP.WEST_GERMAN.PUMA_SDKFZ_234_MP","EBP.WEST_GERMAN.RAKETENWERFER43_88MM_PUPPCHEN_ANTITANK_GUN_MP","EBP.WEST_GERMAN.REINFORCED_BARBED_WIRE_FENCE_MP","EBP.WEST_GERMAN.REINFORCED_BARBED_WIRE_TANK_TRAP_MP","EBP.WEST_GERMAN.SCHU_MINE_42_MP","EBP.WEST_GERMAN.SIPHON_STRUCTURE","EBP.WEST_GERMAN.STURMTIGER_606_38CM_RW_61_MP","EBP.WEST_GERMAN.SWS_HALFTRACK_MP","EBP.WEST_GERMAN.SWS_HALFTRACK_SP","EBP.WEST_GERMAN.TERROR_OFFICER_GUARD_MP","EBP.WEST_GERMAN.TERROR_OFFICER_MP","EBP.WEST_GERMAN.URBAN_ASSAULT_LIGHT_INFANTRY","EBP.WEST_GERMAN.VOLKSGRENADIER_MP","EBP.WEST_GERMAN.WEST_GERMAN_BASE_STAMPER","EBP.WEST_GERMAN.WEST_GERMAN_COMMAND_POST_BARREL","EBP.WEST_GERMAN.WEST_GERMAN_COMMAND_POST_CRATES_01","EBP.WEST_GERMAN.WEST_GERMAN_COMMAND_POST_CRATES_02","EBP.WEST_GERMAN.WEST_GERMAN_COMMAND_POST_GENERATOR","EBP.WEST_GERMAN.WEST_GERMAN_COMMAND_POST_SANDBAG_01","EBP.WEST_GERMAN.WEST_GERMAN_COMMAND_POST_SANDBAG_02","EBP.WEST_GERMAN.WEST_GERMAN_HQ_MP","EBP.WEST_GERMAN.WEST_GERMAN_HQ_WRECK_MP","EBP.WEST_GERMAN.WEST_GERMAN_INVISI_REPAIR_STATION_MP","EBP.WEST_GERMAN.WG_BARBED_WIRE_FENCE_MP","EBP.WEST_GERMAN.WG_SANDBAG_FENCE_MP","SBP.WEST_GERMAN.ARMORED_CAR_SDKFZ_234_SQUAD_MP","SBP.WEST_GERMAN.ASSAULT_PIONEER_SQUAD_MP","SBP.WEST_GERMAN.COMMAND_KING_TIGER_SQUAD_MP","SBP.WEST_GERMAN.FALLSCHIRMJAGER_SQUAD_MP","SBP.WEST_GERMAN.FIELD_OFFICER_SQUAD_MP","SBP.WEST_GERMAN.FLAK_EMPLACEMENT","SBP.WEST_GERMAN.FLAK_EMPLACEMENT_BASE","SBP.WEST_GERMAN.GOLIATH_MP","SBP.WEST_GERMAN.GRW34_81MM_MORTAR_SQUAD_MP","SBP.WEST_GERMAN.HETZER_SQUAD_MP","SBP.WEST_GERMAN.HOWITZER_105MM_LE_FH18_ARTILLERY_MINICHALLENGE","SBP.WEST_GERMAN.HOWITZER_105MM_LONG_RANGE","SBP.WEST_GERMAN.JAEGER_LIGHT_INFANTRY_RECON_SQUAD_MP","SBP.WEST_GERMAN.JAGDPANZER_TANK_DESTROYER_SQUAD_MP","SBP.WEST_GERMAN.JAGDTIGER_TD_SQUAD_MP","SBP.WEST_GERMAN.JU52_PARATROOPER_PLANE","SBP.WEST_GERMAN.JU52_PLANE","SBP.WEST_GERMAN.KING_TIGER_SQUAD_MP","SBP.WEST_GERMAN.KUBELWAGEN_SQUAD_MP","SBP.WEST_GERMAN.LE_IG_18_INF_SUPPORT_GUN_SQUAD_MP","SBP.WEST_GERMAN.MG34_HEAVY_MACHINE_GUN_SQUAD_MP","SBP.WEST_GERMAN.MG42_HEAVY_MACHINE_GUN_SQUAD_WG_MP","SBP.WEST_GERMAN.MORTAR_250_HALFTRACK_SQUAD_WESTGERMAN_MP","SBP.WEST_GERMAN.OBERSOLDATEN_SQUAD_MP","SBP.WEST_GERMAN.OKW_HOWITZER_105MM_LE_FH18_ARTILLERY_MP","SBP.WEST_GERMAN.OSTWIND_SQUAD_WESTGERMAN_MP","SBP.WEST_GERMAN.PAK40_75MM_AT_GUN_SQUAD_WG_MP","SBP.WEST_GERMAN.PAK43_88MM_AT_GUN_SQUAD_WESTGERMAN_MP","SBP.WEST_GERMAN.PANTHER_AUSF_G_SQUAD_MP","SBP.WEST_GERMAN.PANTHER_COMMANDER_SQUAD_MP","SBP.WEST_GERMAN.PANZER_II_LUCHS_SQUAD_MP","SBP.WEST_GERMAN.PANZER_IV_AUSF_J_BATTLE_GROUP_MP","SBP.WEST_GERMAN.PANZERFUSILIER_SQUAD_MP","SBP.WEST_GERMAN.RAKETENWERFER43_88MM_PUPPCHEN_ANTITANK_GUN_SQUAD_MP","SBP.WEST_GERMAN.SCOUTCAR_223_SQUAD","SBP.WEST_GERMAN.SDKFZ_251_17_FLAK_HALFTRACK_SQUAD_MP","SBP.WEST_GERMAN.SDKFZ_251_20_IR_SEARCHLIGHT_HALFTRACK_SQUAD_MP","SBP.WEST_GERMAN.SDKFZ_251_20_IR_SEARCHLIGHT_HALFTRACK_SQUAD_SP","SBP.WEST_GERMAN.SDKFZ_251_HALFTRACK_SQUAD_MP_2","SBP.WEST_GERMAN.SDKFZ_251_WURFRAHMEN_40_HALFTRACK_SQUAD_MP","SBP.WEST_GERMAN.STURMTIGER_SQUAD_MP","SBP.WEST_GERMAN.SWS_HALFTRACK_SQUAD_MP","SBP.WEST_GERMAN.SWS_HALFTRACK_SQUAD_SP","SBP.WEST_GERMAN.TERROR_OFFICER_SQUAD_MP","SBP.WEST_GERMAN.URBAN_ASSAULT_LIGHT_INFANTRY","SBP.WEST_GERMAN.VOLKSGRENADIER_SQUAD_MP","ABILITY.WEST_GERMAN.ADVANCED_SIPHON","ABILITY.WEST_GERMAN.AIRBORNE_ASSAULT","ABILITY.WEST_GERMAN.ARMOR_BLITZ_MP","ABILITY.WEST_GERMAN.ASSAULT_ARTILLERY","ABILITY.WEST_GERMAN.ASSAULT_MOVE_MP","ABILITY.WEST_GERMAN.ASSAULT_PIONEER_BARBED_WIRE_CUTTING_ABILITY_MP","ABILITY.WEST_GERMAN.ASSAULT_PIONEER_DROP_MEDPACK_ABILITY_MP","ABILITY.WEST_GERMAN.BARRAGE_ABILITY_MC","ABILITY.WEST_GERMAN.BASE_BUILDING_RETREAT_POINT_MP","ABILITY.WEST_GERMAN.BLENDKORPER_2H_WAFFEN_ELITE","ABILITY.WEST_GERMAN.BREAKTHROUGH_2","ABILITY.WEST_GERMAN.BREAKTHROUGH_TACTICS","ABILITY.WEST_GERMAN.BUILDING_SELF_DESTRUCT","ABILITY.WEST_GERMAN.BUILDING_SWITCH_FUEL","ABILITY.WEST_GERMAN.BUILDING_SWITCH_MUNITIONS","ABILITY.WEST_GERMAN.COMBAT_BLITZ_MP","ABILITY.WEST_GERMAN.COMMAND_MARK_VEHICLE","ABILITY.WEST_GERMAN.COMMAND_PANTHER","ABILITY.WEST_GERMAN.COMMAND_ROYAL_TIGER_DISPATCH","ABILITY.WEST_GERMAN.CONSTRUCT_ARMORED_INFANTRY_COMMAND","ABILITY.WEST_GERMAN.CONSTRUCT_INFANTRY_BARRACKS","ABILITY.WEST_GERMAN.CONSTRUCT_TANK_COMMAND","ABILITY.WEST_GERMAN.COORDINATED_BARRAGE","ABILITY.WEST_GERMAN.DEFENSIVE_MOVE_MP","ABILITY.WEST_GERMAN.EARLY_WARNING_FLARES","ABILITY.WEST_GERMAN.FALLSCHIRMJAEGER","ABILITY.WEST_GERMAN.FALLSCHIRMJAEGER_GREANDE","ABILITY.WEST_GERMAN.FALLSCHIRMJAEGER_PANZERFAUST","ABILITY.WEST_GERMAN.FALLSCHRIMJAEGER_CAMO","ABILITY.WEST_GERMAN.FATALITY_FLARE_ARTILLERY","ABILITY.WEST_GERMAN.FATALITY_STUKA_FRAGMENTATION_AIRSTRIKE","ABILITY.WEST_GERMAN.FATALITY_STURMTIGER_SATURATION","ABILITY.WEST_GERMAN.FATALITY_WALKING_STUKA_BARRAGE","ABILITY.WEST_GERMAN.FIELD_DEFENSES","ABILITY.WEST_GERMAN.FLAK_EMPLACEMENT_SELF_REPAIR","ABILITY.WEST_GERMAN.FLAK_HALFTRACK_CONCEALING_SMOKE_MP","ABILITY.WEST_GERMAN.FLAME_HALTRACK_DISPATCH","ABILITY.WEST_GERMAN.FLARE_ARTILLERY","ABILITY.WEST_GERMAN.FLARE_TRAP_CAPTURE_POINT","ABILITY.WEST_GERMAN.FOR_THE_FATHERLAND","ABILITY.WEST_GERMAN.FORTIFY_POSITION_MP","ABILITY.WEST_GERMAN.FORWARD_RECIEVERS","ABILITY.WEST_GERMAN.GOLIATH_DISPATCH","ABILITY.WEST_GERMAN.GRW34_MORTAR_COUNTER_BARRAGE_ATTACK_MP","ABILITY.WEST_GERMAN.GRW34_MORTAR_COUNTER_BARRAGE_WEAPON_WG_MP","ABILITY.WEST_GERMAN.GRW34_MORTAR_TEAM_MORTAR_BARRAGE_WG_MP","ABILITY.WEST_GERMAN.GRW34_MORTAR_TEAM_MORTAR_VICTORTARGET_BARRAGE_WG_MP","ABILITY.WEST_GERMAN.GRW34_MORTAR_TEAM_SMOKE_BARRAGE_WG_MP","ABILITY.WEST_GERMAN.HEAT_SHELLS_ABILITY_MP","ABILITY.WEST_GERMAN.HEAT_SHELLS_UNLOCK","ABILITY.WEST_GERMAN.HEAVY_FORTIFICATIONS","ABILITY.WEST_GERMAN.HETZER_DISPATCH","ABILITY.WEST_GERMAN.HOWITZER_105MM_EMPLACEMENT_UNLOCK_OKW","ABILITY.WEST_GERMAN.HOWITZER_105MM_LONG_RANGE_BARRAGE","ABILITY.WEST_GERMAN.HOWITZER_105MM_OFFMAP_BARRAGE","ABILITY.WEST_GERMAN.HOWITZER_TOGGLE_FIRE_PM","ABILITY.WEST_GERMAN.INFILTRATION_TACTICS_GRENADE","ABILITY.WEST_GERMAN.INFILTRATION_TACTICS_UNLOCK","ABILITY.WEST_GERMAN.INFRARED_STG44","ABILITY.WEST_GERMAN.JAEGER_BOOBY_TRAP","ABILITY.WEST_GERMAN.JAEGER_LIGHT_INFANTRY_CAMO","ABILITY.WEST_GERMAN.JAEGER_LIGHT_INFANTRY_RECON_DISPATCH","ABILITY.WEST_GERMAN.JAGDTIGER","ABILITY.WEST_GERMAN.JAGDTIGER_128MM_SUPPORTING_FIRE","ABILITY.WEST_GERMAN.JAGDTIGER_PIERCING_SHELL_ABILITY_MP","ABILITY.WEST_GERMAN.KING_TIGER_COMMAND_MODE_MP","ABILITY.WEST_GERMAN.KING_TIGER_DISPATCH","ABILITY.WEST_GERMAN.KUBELWAGEN_DETECTION_MP","ABILITY.WEST_GERMAN.KUBELWAGEN_HOLD_FIRE_MP","ABILITY.WEST_GERMAN.KUBELWAGEN_IN_COVER_AUTO_CAMOUFLAGE_MP","ABILITY.WEST_GERMAN.LE_IG_18_BARRAGE_WG_MP","ABILITY.WEST_GERMAN.LE_IG_18_BARRAGE_WG_VET_MP","ABILITY.WEST_GERMAN.LE_IG_18_HOLLOW_CHARGE_BARRAGE_WG_MP","ABILITY.WEST_GERMAN.LE_IG_18_HOLLOW_CHARGE_BARRAGE_WG_VET_MP","ABILITY.WEST_GERMAN.MG34_DISPATCH","ABILITY.WEST_GERMAN.MG34_PHOSPHORUS_ROUNDS_MP","ABILITY.WEST_GERMAN.MINESWEEPER_DEPLOY_MP","ABILITY.WEST_GERMAN.MINESWEEPER_PUT_AWAY_MP","ABILITY.WEST_GERMAN.MORTAR_HALFTRACK_WEST_GERMAN","ABILITY.WEST_GERMAN.OFFMAP_NEBEL_BARRAGE_MP","ABILITY.WEST_GERMAN.OKW_HOLD_FIRE_MP","ABILITY.WEST_GERMAN.OKW_RATKEN_VEHICLE_HOLD_FIRE_MP","ABILITY.WEST_GERMAN.OKW_SECTOR_ASSAULT","ABILITY.WEST_GERMAN.OKW_STUKA_AERIAL_SUPERIORITY_RECON","ABILITY.WEST_GERMAN.OKW_VEHICLE_HOLD_FIRE_MP","ABILITY.WEST_GERMAN.OSTWIND_DISPATCH","ABILITY.WEST_GERMAN.PAK40_CRITICAL_SHOTS_WG_MP","ABILITY.WEST_GERMAN.PANZER_IV_GROUP_DISPATCH","ABILITY.WEST_GERMAN.PANZERFUSILIER_AT_RIFLE_GRENADE","ABILITY.WEST_GERMAN.PANZERFUSILIER_GRENADE","ABILITY.WEST_GERMAN.PANZERFUSILIERS_DISPATCH","ABILITY.WEST_GERMAN.PANZERFUSILIERS_FLARE","ABILITY.WEST_GERMAN.PIONEER_STUN_GRENADE_MP","ABILITY.WEST_GERMAN.PIONEER_VOLKS_SALVAGE","ABILITY.WEST_GERMAN.PIONEER_VOLKS_THROUGH_SALVAGE","ABILITY.WEST_GERMAN.PUMA_AIMED_SHOT_MP","ABILITY.WEST_GERMAN.PUMA_SMOKE_SCREEN","ABILITY.WEST_GERMAN.PYRO_VOLKS","ABILITY.WEST_GERMAN.RADIO_SILENCE","ABILITY.WEST_GERMAN.RAKETEN_IN_COVER_AUTO_CAMOUFLAGE_MP","ABILITY.WEST_GERMAN.RAKTEN_CAMOUFLAGE_MP","ABILITY.WEST_GERMAN.RECON_STANCE_MP","ABILITY.WEST_GERMAN.RECOUP_LOSSES","ABILITY.WEST_GERMAN.REFUEL_TANK_WG_SP","ABILITY.WEST_GERMAN.ROCKET_BARRAGE","ABILITY.WEST_GERMAN.SDKFZ_251_17_FLAK_HALFTRACK_DEPLOY_DEFENS","ABILITY.WEST_GERMAN.SDKFZ_251_17_FLAK_HALFTRACK_DEPLOY_WEAPON","ABILITY.WEST_GERMAN.SDKFZ_251_17_FLAK_HALFTRACK_DEPLOY_WEAPON_VET","ABILITY.WEST_GERMAN.SIGNAL_FLAGS","ABILITY.WEST_GERMAN.SIPHON_INCREASE_RESOURCES_ADVANCED_MP","ABILITY.WEST_GERMAN.SIPHON_INCREASE_RESOURCES_MP","ABILITY.WEST_GERMAN.SPEARHEAD_MP","ABILITY.WEST_GERMAN.STALKER_STATE_MP","ABILITY.WEST_GERMAN.STURMTIGER_380MM_ROCKET_ATTACK","ABILITY.WEST_GERMAN.STURMTIGER_380MM_ROCKET_RELOAD","ABILITY.WEST_GERMAN.STURMTIGER_DISPATCH","ABILITY.WEST_GERMAN.STURMTIGER_NAHVW_CLOSE_RANGE_GRENADE_TARGETED","ABILITY.WEST_GERMAN.SUPPORT_TRUCK_GAIN_RESOURCECS","ABILITY.WEST_GERMAN.SUPPORT_TRUCK_TARGET_SETUP","ABILITY.WEST_GERMAN.SUPPORT_TRUCK_TARGET_UNSETUP","ABILITY.WEST_GERMAN.SUPPRESSIVE_FIRE_MP","ABILITY.WEST_GERMAN.SWS_HALFTRACK_DISPATCH","ABILITY.WEST_GERMAN.SWS_HALFTRACK_FORWARD_RECEIVERS","ABILITY.WEST_GERMAN.SWS_HALFTRACK_INTERVAL_DISPATCH","ABILITY.WEST_GERMAN.TANK_COMMANDER_UNLOCK","ABILITY.WEST_GERMAN.TANK_THROW_DEFENSIVE_GRENADE_MP","ABILITY.WEST_GERMAN.TANK_THROW_DEFENSIVE_GRENADE_UNLOCK_MP","ABILITY.WEST_GERMAN.TERROR_OFFICER","ABILITY.WEST_GERMAN.TERROR_OFFICER_FORCE_RETREAT","ABILITY.WEST_GERMAN.TERROR_OFFICER_MARK_TARGET","ABILITY.WEST_GERMAN.THROUGH_SALVAGE","ABILITY.WEST_GERMAN.TIGER_PROWL_JAGDPANZER_MP","ABILITY.WEST_GERMAN.TIGER_PROWL_MP","ABILITY.WEST_GERMAN.URBAN_ASSAULT_LIGHT_INFANTRY","ABILITY.WEST_GERMAN.URBAN_ASSAULT_LIGHT_INFANTRY_THROW_ABILITY_MP","ABILITY.WEST_GERMAN.VALIANT_ASSAULT","ABILITY.WEST_GERMAN.VEHICLE_CRITICAL_REPAIR_UNLOCK","ABILITY.WEST_GERMAN.VEHICLE_EMERGENCY_REPAIR_ABILITY_MP","ABILITY.WEST_GERMAN.VEHICLE_EMERGENCY_REPAIR_ABILITY_SWS_MP","ABILITY.WEST_GERMAN.VOLKS_PANZERFAUST_MP","ABILITY.WEST_GERMAN.VOLKSGRENADIER_FIRE_GRENADE_MP","ABILITY.WEST_GERMAN.VOLKSGRENADIER_GRENADE_MP","ABILITY.WEST_GERMAN.VOLKSGRENADIER_PANZERFAUST_MP","ABILITY.WEST_GERMAN.VOLKSGRENADIER_PANZERFAUST_VET_4_MP","ABILITY.WEST_GERMAN.WAFFEN_BOOBY_TRAP_CAPTURE_POINT","ABILITY.WEST_GERMAN.WAFFEN_ELITE_BUNDLED_ASSAULT_GRENADE","ABILITY.WEST_GERMAN.WALKING_STUKA_ROCKET_BARRAGE_CREEPING_MP","ABILITY.WEST_GERMAN.WALKING_STUKA_ROCKET_BARRAGE_CREEPING_NAPALM_MP","ABILITY.WEST_GERMAN.WEST_GERMAN_REPAIR_ABILITY_MP","ABILITY.WEST_GERMAN.WG_HQ_PIONEER_CALL_IN","ABILITY.WEST_GERMAN.ZEROING_ARTILLERY","UPG.WEST_GERMAN.ABILITY_LOCK_OUT_STURMTIGER_NOT_RELOADED","UPG.WEST_GERMAN.ABILITY_LOCK_OUT_STURMTIGER_RELOADING","UPG.WEST_GERMAN.ABILITY_LOCK_OUT_SWS_TRUCK","UPG.WEST_GERMAN.ADVANCED_SIPHON","UPG.WEST_GERMAN.AERIAL_SUPERIORITY_STUKA_RECON_PLANE","UPG.WEST_GERMAN.AIRBORNE_ASSAULT","UPG.WEST_GERMAN.ASSAULT_ARTILLERY","UPG.WEST_GERMAN.ASSAULT_PIONEER_COMBAT_UPGRADE","UPG.WEST_GERMAN.ASSAULT_PIONEER_PANZERSCHRECK_UPGRADE","UPG.WEST_GERMAN.ASSAULT_PIONEER_REPAIR_UPGRADE","UPG.WEST_GERMAN.BREAKTHROUGH_2","UPG.WEST_GERMAN.BREAKTHROUGH_TACTICS","UPG.WEST_GERMAN.BUILDING_1","UPG.WEST_GERMAN.BUILDING_2","UPG.WEST_GERMAN.BUILDING_3","UPG.WEST_GERMAN.COMMAND_PANTHER","UPG.WEST_GERMAN.COMMAND_ROYAL_TIGER_DISPATCH","UPG.WEST_GERMAN.CONSTRUCT_BASE_BUILDING_UPGRADE","UPG.WEST_GERMAN.FALLSCHRIMJAGER_DISPATCH","UPG.WEST_GERMAN.FIELD_DEFENSES","UPG.WEST_GERMAN.FIRST_SWS_HALFTRACK_LOCKOUT","UPG.WEST_GERMAN.FLAK_GUN_UNLOCK_UPGRADE","UPG.WEST_GERMAN.FLAK_PANZER_DEFENSIVES","UPG.WEST_GERMAN.FLAK_PANZER_IS_SETUP","UPG.WEST_GERMAN.FLAME_HALFTRACK_DISPATCH","UPG.WEST_GERMAN.FLAMMPANZER_38T_HETZER","UPG.WEST_GERMAN.FLARE_ARTILLERY","UPG.WEST_GERMAN.FOR_THE_FATHER_LAND","UPG.WEST_GERMAN.FORWARD_RECIEVERS","UPG.WEST_GERMAN.GOLIATH_REMOTE_CONTROLLED_BOMB","UPG.WEST_GERMAN.HEALING_POINT_UNLOCK_UPGRADE","UPG.WEST_GERMAN.HEAT_SHELLS","UPG.WEST_GERMAN.HEAVY_FORTIFICATIONS","UPG.WEST_GERMAN.HOWITZER_105MM_EMPLACEMENT_OKW","UPG.WEST_GERMAN.HOWITZER_105MM_OFFMAP_BARRAGE","UPG.WEST_GERMAN.INFILTRATION_TACTICS","UPG.WEST_GERMAN.INFRARED_STG44","UPG.WEST_GERMAN.JAEGER_LIGHT_INFANTRY_RECON_DISPATCH","UPG.WEST_GERMAN.JAGDTIGER","UPG.WEST_GERMAN.JAGDTIGER_ABILITY_AP_LOCK_OUT","UPG.WEST_GERMAN.JAGDTIGER_ABILITY_BARRAGE_LOCK_OUT","UPG.WEST_GERMAN.JAGDTIGER_ENGINE_IMPROVEMENTS_I_MP","UPG.WEST_GERMAN.KING_TIGER_TOP_GUNNER_MP","UPG.WEST_GERMAN.MEDIC_HEALING_MP","UPG.WEST_GERMAN.MEDICAL_SUPPLIES_0_USES_REMAINING","UPG.WEST_GERMAN.MEDICAL_SUPPLIES_1_USE_REMAINING","UPG.WEST_GERMAN.MEDICAL_SUPPLIES_2_USES_REMAINING","UPG.WEST_GERMAN.MG34_DISPATCH","UPG.WEST_GERMAN.OKW_SECTOR_ASSAULT","UPG.WEST_GERMAN.OSTWIND_DISPATCH","UPG.WEST_GERMAN.PANZER_IV_GROUP_DISPATCH","UPG.WEST_GERMAN.PANZER_IV_SIDE_SKIRTS_MP","UPG.WEST_GERMAN.PANZERFUSILER_DISPATCH","UPG.WEST_GERMAN.PANZERFUSILIER_G43","UPG.WEST_GERMAN.PANZERSCHRECK_UNLOCKED","UPG.WEST_GERMAN.PYRO_VOLKS","UPG.WEST_GERMAN.RADIO_SILENCE","UPG.WEST_GERMAN.RECOUP_ACTIVE","UPG.WEST_GERMAN.RECOUP_LOSS","UPG.WEST_GERMAN.REPAIR_ENGINEERS_MP","UPG.WEST_GERMAN.REPAIR_POINT_UNLOCK_UPGRADE","UPG.WEST_GERMAN.RESOURCE_POINT_SIPHON","UPG.WEST_GERMAN.RETREAT_POINT_UNLOCK_UPGRADE","UPG.WEST_GERMAN.ROCKET_BARRAGE","UPG.WEST_GERMAN.SDKFZ_251_HALFTRACK_FLAMMPANZERWAGEN_UPGRADE_MP_2","UPG.WEST_GERMAN.SIGNAL_FLAGS","UPG.WEST_GERMAN.SIPHON_LOCK_OUT","UPG.WEST_GERMAN.STURMTIGER_DISPATCH","UPG.WEST_GERMAN.SWS_INTERVAL_UNLOCK","UPG.WEST_GERMAN.SWS_STARTING_DISPATCH_UNLOCK","UPG.WEST_GERMAN.TANK_COMMANDER","UPG.WEST_GERMAN.TANK_COMMANDER_UNLOCK","UPG.WEST_GERMAN.TANK_GRENADE","UPG.WEST_GERMAN.TERROR_OFFICER","UPG.WEST_GERMAN.THROUGH_SALVAGE","UPG.WEST_GERMAN.URBAN_ASSAULT_LIGHT_INFANTRY","UPG.WEST_GERMAN.VALIANT_ASSAULT","UPG.WEST_GERMAN.VEHICLE_CRITICAL_REPAIR","UPG.WEST_GERMAN.VOLKS_FLAMETHROWER_MP","UPG.WEST_GERMAN.VOLKS_STG44_UPGRADE","UPG.WEST_GERMAN.WAFFEN_INFRARED_STG44","UPG.WEST_GERMAN.WAFFEN_MG34_LMG_MP","UPG.WEST_GERMAN.WARNING_FLARES","UPG.WEST_GERMAN.WG_HETZER_TOP_GUNNER_MP","UPG.WEST_GERMAN.WG_PANTHER_TOP_GUNNER_MP","UPG.WEST_GERMAN.ZEROING_ARTILLERY","ABILITY.GLOBAL.ARMY_ITEM_GLOBAL_COVER_TRAINING","ABILITY.GLOBAL.ARMY_ITEM_SOVIET_NOT_GONNA_DIE_LIKE_THIS","ABILITY.GLOBAL.AT_76MM_SINGLE_SHOT_ACCURATE","ABILITY.GLOBAL.BLIZZARD_EFFECT","ABILITY.GLOBAL.BLIZZARD_EFFECT_DEEP_SNOW_CAMO","ABILITY.GLOBAL.BLIZZARD_EFFECT_MORTARS","ABILITY.GLOBAL.BLIZZARD_EFFECT_VEHICLE","ABILITY.GLOBAL.BLIZZARD_HOWITZER","ABILITY.GLOBAL.BONUS_0","ABILITY.GLOBAL.BONUS_1","ABILITY.GLOBAL.BONUS_2","ABILITY.GLOBAL.BONUS_2B","ABILITY.GLOBAL.BONUS_3","ABILITY.GLOBAL.BONUS_3B","ABILITY.GLOBAL.BONUS_3C","ABILITY.GLOBAL.BONUS_BACK","ABILITY.GLOBAL.BREAKTHROUGH_TOW","ABILITY.GLOBAL.CAMOUFLAGE_CONSTRUCTION","ABILITY.GLOBAL.CAMOUFLAGE_CONSTRUCTION_ANIA","ABILITY.GLOBAL.CAMPAIGN_STUKA_STRAFE_LONG","ABILITY.GLOBAL.CAPTURE_SPEED","ABILITY.GLOBAL.COMMISSAR_SHOT_227","ABILITY.GLOBAL.COMMISSAR_SHOT_227_ENEMY","ABILITY.GLOBAL.COMMISSAR_SQUAD_TOW","ABILITY.GLOBAL.CONVOY_BUILDBARRICADE","ABILITY.GLOBAL.COVER_ANIMATION_TEST","ABILITY.GLOBAL.DIG_OUT_OF_MUD","ABILITY.GLOBAL.DISPATCH_BRIDGE_PARTISAN","ABILITY.GLOBAL.DISPATCH_BRIDGE_PARTISAN_AT","ABILITY.GLOBAL.DISPATCH_BRIDGE_PARTISAN_HMG","ABILITY.GLOBAL.DISPATCH_BRIDGE_PARTISAN_MORTAR","ABILITY.GLOBAL.DROP_WEAPONS","ABILITY.GLOBAL.FATALITY_BULLSEYE","ABILITY.GLOBAL.FATALITY_COORDINATED_MORTAR_BOMBARDMENT","ABILITY.GLOBAL.FATALITY_DEFAULT","ABILITY.GLOBAL.FATALITY_HOWITZER_105MM_BARRAGE","ABILITY.GLOBAL.FATALITY_HOWITZER_240MM","ABILITY.GLOBAL.FATALITY_LIGHT_SUPPORT_ARTILLERY","ABILITY.GLOBAL.FATALITY_PROTOTYPE","ABILITY.GLOBAL.FATALITY_RAILWAY_GUN_ARTILLERY","ABILITY.GLOBAL.FATALITY_TIME_ON_TARGET_ARTILLERY","ABILITY.GLOBAL.FIRE_DOT","ABILITY.GLOBAL.FLAME_THROWER_ABILITY","ABILITY.GLOBAL.FORWARD_REPAIR_STATION_TOW","ABILITY.GLOBAL.FROZEN_ICON_TEST","ABILITY.GLOBAL.GARRISONED_SQUAD_FACING","ABILITY.GLOBAL.GARRISONED_SQUAD_FACING_UNSET","ABILITY.GLOBAL.HEAL_IN_COVER","ABILITY.GLOBAL.HOWITZER_105MM_BARRAGE_SHORT","ABILITY.GLOBAL.HOWITZER_105MM_BARRAGE_SHORT_PRECISE","ABILITY.GLOBAL.HOWITZER_105MM_DUMMY","ABILITY.GLOBAL.IL_2_ATTACK_STRAFE_HMG","ABILITY.GLOBAL.IL_2_PRECISION_BOMB_STRIKE_TOW","ABILITY.GLOBAL.KV_2_TOW","ABILITY.GLOBAL.LIGHT_ARTILLERY_M10","ABILITY.GLOBAL.M01_IL2_DOGFIGHT_PASS","ABILITY.GLOBAL.M01_IL2_PRECISION_BOMB_STRIKE","ABILITY.GLOBAL.M01_MEDIC_HEAL","ABILITY.GLOBAL.M01_MEDIC_HEAL_CONSTANT","ABILITY.GLOBAL.M01_MORTAR_SINGLE_PRECISE_HARMLESS","ABILITY.GLOBAL.M01_SPRINT_OUT_OF_COMBAT","ABILITY.GLOBAL.M01_STUKA_BOMBING_STRIKE","ABILITY.GLOBAL.M01_STUKA_DOGFIGHT_PASS","ABILITY.GLOBAL.M01_STUKA_STRAFE_FAST","ABILITY.GLOBAL.M01_WOUNDED","ABILITY.GLOBAL.M11_LIGHT_FIRE","ABILITY.GLOBAL.M12_HOWITZER_BARRAGE","ABILITY.GLOBAL.M14_GUARD_TROOP_DISPATCH","ABILITY.GLOBAL.M14_OFF_MAP_SMOKE_BARRAGE","ABILITY.GLOBAL.M24_ANTI_TANK_BUNDLED_GRENADE","ABILITY.GLOBAL.MECHANIZED_ASSAULT_GROUP_TOW","ABILITY.GLOBAL.MOLTKE_DET_PACK","ABILITY.GLOBAL.MUDDY_POINT","ABILITY.GLOBAL.NO_RETREAT_NO_SURRENDER_TOW","ABILITY.GLOBAL.OFF_MAP_ARTILLERY","ABILITY.GLOBAL.OFF_MAP_ARTILLERY_PERCISE","ABILITY.GLOBAL.OFF_MAP_ARTILLERY_PERCISE_FAST","ABILITY.GLOBAL.OFF_MAP_ARTILLERY_PERCISE_SEP","ABILITY.GLOBAL.OFF_MAP_ARTY_SINGLE_SHOT_INSTANT","ABILITY.GLOBAL.OFFICER_AIR_RECON","ABILITY.GLOBAL.OFFICER_CLOSE_AIR_SUPPORT","ABILITY.GLOBAL.OFFICER_FRAGMENTATION_BOMB","ABILITY.GLOBAL.PARTISAN_REPAIR_ABILITY","ABILITY.GLOBAL.PARTISAN_SPRINT","ABILITY.GLOBAL.PREVENT_SUPPRESSION","ABILITY.GLOBAL.PRODUCTION_SPEED","ABILITY.GLOBAL.RADIO_TOWER_REVEAL","ABILITY.GLOBAL.RAILWAY_GUN_ARTILLERY_SINGLE","ABILITY.GLOBAL.READY_UP","ABILITY.GLOBAL.REV_OUT_OF_MUD","ABILITY.GLOBAL.SHOCK_TROOP_FULL_AUTO","ABILITY.GLOBAL.SP_DROP_WEAPONS","ABILITY.GLOBAL.SP_OFF_MAP_ARTY_HARMLESS","ABILITY.GLOBAL.SP_OFF_MAP_ARTY_REAL","ABILITY.GLOBAL.SP_SINGLE_SHOT_MORTAR","ABILITY.GLOBAL.SP_SINGLE_SHOT_MORTAR_M01","ABILITY.GLOBAL.SP_SPRINT","ABILITY.GLOBAL.SP_SPRINT_TOGGLEABLE","ABILITY.GLOBAL.SPY_NETWORK_TOW","ABILITY.GLOBAL.STUKA_BOMBING_STRIKE_W_SMOKE","ABILITY.GLOBAL.STUKA_FAKE_BOMBING_STRIKE","ABILITY.GLOBAL.STUKA_FAKE_STRAFE","ABILITY.GLOBAL.STUKA_STRAFE","ABILITY.GLOBAL.STUKA_STRAFE_M02","ABILITY.GLOBAL.STUKA_STRAFE_M09","ABILITY.GLOBAL.TANK_BUSTER_CONSCRIPT_DISPATCH","ABILITY.GLOBAL.TOW_AIRFIELD_DISPATCH_KV1","ABILITY.GLOBAL.TOW_AIRFIELD_DISPATCH_KV2","ABILITY.GLOBAL.TOW_AIRFIELD_DISPATCH_KV8","ABILITY.GLOBAL.TOW_AIRFIELD_DISPATCH_T34","ABILITY.GLOBAL.TOW_AIRFIELD_STUKA_BOMBING_RUN","ABILITY.GLOBAL.TOW_STALINGRAD_DISPATCH_IS2","ABILITY.GLOBAL.TOW_STALINGRAD_DISPATCH_KAT","ABILITY.GLOBAL.TOW_STALINGRAD_DISPATCH_KV1","ABILITY.GLOBAL.TOW_STALINGRAD_DISPATCH_SU76","ABILITY.GLOBAL.TOW_STALINGRAD_DISPATCH_T34","ABILITY.GLOBAL.TOW_STALINGRAD_DISPATCH_T70","ABILITY.GLOBAL.TRANSFER_ORDERS","ABILITY.GLOBAL.TROOP_TRAINING_TOW","ABILITY.GLOBAL.TUNSTEN_SHELLS_TOW","ABILITY.GLOBAL.WARMING_ANIMATION_TEST","ABILITY.GLOBAL.WE_SURRENDER","SLOT_ITEM.AEC_TARGET_OPTICS_SLOT_ITEM_MP","SLOT_ITEM.AEC_TARGET_TURRET_SLOT_ITEM_MP","SLOT_ITEM.AEC_TREAD_SHOT_MP","SLOT_ITEM.AEF_CALLIOPE_DUMMY_SLOT_ITEM","SLOT_ITEM.AEF_SHERMAN_DUMMY_SLOT_ITEM","SLOT_ITEM.AEF_VEHICLE_ENTERS_INFANTRY_BUFF_APPLIED","SLOT_ITEM.AEF_WHITE_PHOSPHOROUS_MORTAR_UI_ITEM","SLOT_ITEM.AEF_WHITE_PHOSPHOROUS_SHELLS_UI_ITEM","SLOT_ITEM.AEF_WRENCH_ICON_SLOT_ITEM","SLOT_ITEM.AMBUSH_CAMO_PORTRAIT_ICON_ITEM","SLOT_ITEM.AMBUSH_CAMO_SLOT_ITEM","SLOT_ITEM.AMBUSH_CAMO_VISUAL_ITEM","SLOT_ITEM.ARMOR_BLITZ_ITEM","SLOT_ITEM.ASSAULT_ENGINEER_FLAMETHROWER","SLOT_ITEM.ASSAULT_MOVE_ITEM","SLOT_ITEM.AT_76MM_HE_ROUND_ITEM","SLOT_ITEM.AT_76MM_HE_ROUND_ITEM_MP","SLOT_ITEM.AVRE_CREW_SHRAPNEL_GRENADE_SLOT_ITEM_MP","SLOT_ITEM.AVRE_RELOAD_ACTIVE","SLOT_ITEM.AVRE_SPIGOT_MORTAR_MP","SLOT_ITEM.AVRE_SPIGOT_MORTAR_VET_3_MP","SLOT_ITEM.AXIS_ASSAULT_GRENADIER_GRENADE","SLOT_ITEM.AXIS_BLINDING_GRENADE","SLOT_ITEM.AXIS_BLINDING_GRENADE_MP","SLOT_ITEM.AXIS_PANZER_GRENADIER_GRENADE","SLOT_ITEM.AXIS_PANZER_GRENADIER_GRENADE_MP","SLOT_ITEM.AXIS_PG_GRENADE_CAMPAIGN","SLOT_ITEM.AXIS_PG_GRENADE_CAMPAIGN_MP","SLOT_ITEM.AXIS_PG_GRENADE_TUTORIAL","SLOT_ITEM.BAZOOKA_MP","SLOT_ITEM.BLENDKORPER_2H_SMOKE_GRENADE_ITEM_MP","SLOT_ITEM.BOFOR_40MM_AA_MODE_ACTIVATED_MAIN_GUN","SLOT_ITEM.BOFORS_HOLD_FULL","SLOT_ITEM.BOFORS_SUPPRESSIVE_BARRAGE_ROUND_ITEM_MP","SLOT_ITEM.BOFORS_SUPPRESSIVE_BARRAGE_ROUND_ITEM_VICTOR_TARGET_MP","SLOT_ITEM.BOOT_STOMP","SLOT_ITEM.BOYS_ANTI_TANK_RIFLE_MP","SLOT_ITEM.BOYS_ANTI_TANK_RIFLE_SNIPER_MP","SLOT_ITEM.BOYS_SNIPER_RIFLE_ITEM_MP","SLOT_ITEM.BREN_LMG_ICON_DUMMY","SLOT_ITEM.BRIT_17_POUNDER_FLARE_MP","SLOT_ITEM.BRIT_17_POUNDER_HOLD_FULL","SLOT_ITEM.BRIT_17_POUNDER_PIERCING_SHOT_MP","SLOT_ITEM.BRIT_COMMAND_VEHICLE_ITEM","SLOT_ITEM.BRIT_CROC_DUMMY_SLOT_ITEM","SLOT_ITEM.BRIT_EMPLACEMENT_BRACED","SLOT_ITEM.BRIT_EMPLACEMENT_HOLD_FIRE","SLOT_ITEM.BRIT_FIREFLY_TULIP_SLOT_ITEM","SLOT_ITEM.BRIT_HOLD_THE_LINE","SLOT_ITEM.BRIT_MORTAR_PIT_HOLD_FULL","SLOT_ITEM.BRIT_REINFORCE_THE_FRONT","SLOT_ITEM.BRIT_SNIPER_BOYS_ANTI_TANK_CRITICAL_SHOT_MP","SLOT_ITEM.BRIT_UNIT_LOCK_OUT_SLOT_ITEM","SLOT_ITEM.BRUMMBAR_CRITICAL_SHOT_MP","SLOT_ITEM.CAPTAIN_GARRISON_ITEM","SLOT_ITEM.CAPTURE_INTEL_SLOTITEM","SLOT_ITEM.CARRIER_SUPPRESS_ACTIVE","SLOT_ITEM.CAVALRY_AT_SATCHEL_ITEM","SLOT_ITEM.CENTUAR_AA_MODE_ACTIVATED_MAIN_GUN","SLOT_ITEM.CHURUCHILL_SUPPORT_NEGATE","SLOT_ITEM.COMET_SMOKE_SHELL_SHOT_MP","SLOT_ITEM.COMET_SMOKE_SHELL_WP_SHOT_MP","SLOT_ITEM.COMMAND_PANTHER_AURA","SLOT_ITEM.COMMANDO_BREN_LMG_MP","SLOT_ITEM.COMMANDO_DE_LISLE_CARBINE_MP","SLOT_ITEM.COMMANDO_DE_LISLE_CARBINE_SLOT_MP","SLOT_ITEM.COMMANDO_N69_GRENADE_MP","SLOT_ITEM.COMMANDO_THOMPSON_MP","SLOT_ITEM.COMMANDO_THOMPSON_SLOT_MP","SLOT_ITEM.COMMISSAR_SHOT_227","SLOT_ITEM.COMMISSAR_SHOT_227_ENEMY","SLOT_ITEM.CONSCRIPT_MOLOTOV","SLOT_ITEM.CONSCRIPT_MOLOTOV_MP","SLOT_ITEM.COVER_SMOKE_GRENADE_ITEM","SLOT_ITEM.DEF_MOVE_ITEM","SLOT_ITEM.DOUBLE_SWEEP","SLOT_ITEM.DP_28_LIGHT_MACHINE_GUN_PACKAGE","SLOT_ITEM.DP_28_LIGHT_MACHINE_GUN_PACKAGE_MOVING_MP","SLOT_ITEM.DP_28_LIGHT_MACHINE_GUN_PACKAGE_MOVING_NO_PRONE_MP","SLOT_ITEM.DP_28_LIGHT_MACHINE_GUN_PACKAGE_MP","SLOT_ITEM.DSHK38_TURRET_MOUNTED_IS2","SLOT_ITEM.DSHK38_TURRET_MOUNTED_IS2_MP","SLOT_ITEM.DSHK38_TURRET_MOUNTED_ISU152","SLOT_ITEM.DSHK38_TURRET_MOUNTED_ISU152_MP","SLOT_ITEM.DUMMY_FORTIFIED__SLOT_ITEM","SLOT_ITEM.DUMMY_SLOT_ITEM","SLOT_ITEM.DUMMY_SLOT_ITEM_QUAD","SLOT_ITEM.ELEFANT_CRITICAL_SHOT_MP","SLOT_ITEM.ENGINEER_SALVAGE_KIT_DUMMY","SLOT_ITEM.FLAK_HALFTRACK_ICON_ITEM","SLOT_ITEM.FLAMETHROWER_ROKS3_ACCESSORY","SLOT_ITEM.FLAMETHROWER_ROKS3_FAKE","SLOT_ITEM.FLAMETHROWER_ROKS3_ITEM","SLOT_ITEM.FLAMETHROWER_ROKS3_ITEM_MP","SLOT_ITEM.FOR_THE_FATHERLAND_ACTIVE","SLOT_ITEM.FRWD_HQ_SMOKE_MARKER_GRENADE_MP","SLOT_ITEM.FWD_HQ_EMPLACEMENT_SUPPORT","SLOT_ITEM.G43_SNIPER_INCENDIARY_SLOT_ITEM_MP","SLOT_ITEM.GENERIC_MG34_LMG_MP","SLOT_ITEM.GRENADIER_MG42_LMG","SLOT_ITEM.GRENADIER_MG42_LMG_MOVING_MP","SLOT_ITEM.GRENADIER_MG42_LMG_MOVING_NO_PRONE_MP","SLOT_ITEM.GRENADIER_MG42_LMG_MP","SLOT_ITEM.GRENADIER_PANZERFAUST","SLOT_ITEM.GRENADIER_PANZERFAUST_MP","SLOT_ITEM.GROUND_ATTACK_SNIPER_RIFLE_ITEM","SLOT_ITEM.GUARD_TROOP_ASSAULT_PACKAGE","SLOT_ITEM.HALFTRACK_FLAMETHROWER_LEFT","SLOT_ITEM.HALFTRACK_FLAMETHROWER_LEFT_MP","SLOT_ITEM.HALFTRACK_FLAMETHROWER_RIGHT","SLOT_ITEM.HALFTRACK_FLAMETHROWER_RIGHT_MP","SLOT_ITEM.HETZER_FLAMETHROWER_ITEM_MP","SLOT_ITEM.HULLDOWN_SLOT_ITEM","SLOT_ITEM.INFRARED_SQUAD_SETUP","SLOT_ITEM.ISU_PIERCING_SHOT_ROUND_ITEM","SLOT_ITEM.ISU_PIERCING_SHOT_ROUND_ITEM_MP","SLOT_ITEM.JAEGER_G43_RIFLE_ITEM","SLOT_ITEM.JAEGER_G43_RIFLE_ITEM_MP","SLOT_ITEM.JAEGER_LIGHT_RECON_G43","SLOT_ITEM.JAEGER_PANZERGREN_G43_RIFLE_ITEM_MP","SLOT_ITEM.KAR_98K_ANTITANK_RIFLE_GRENADE_SLOT_ITEM","SLOT_ITEM.KAR_98K_ANTITANK_RIFLE_GRENADE_SLOT_ITEM_MP","SLOT_ITEM.KAR_98K_RIFLE_GRENADE_SLOT_ITEM","SLOT_ITEM.KAR_98K_RIFLE_GRENADE_SLOT_ITEM_MP","SLOT_ITEM.KAR_98K_RIFLE_GRENADE_SLOT_ITEM_TUTORIAL","SLOT_ITEM.KV_8_45MM_GUN_ITEM","SLOT_ITEM.KV_8_ATO_41_FLAMETHROWER_ITEM_MP","SLOT_ITEM.KWK_20MM_222_ARMORED_CAR_MP","SLOT_ITEM.LAND_MATTRESS_25LB_ROCKET","SLOT_ITEM.LAND_MATTRESS_60LB_ROCKET","SLOT_ITEM.LAND_MATTRESS_EMPTY","SLOT_ITEM.LAND_MATTRESS_PHOSPHORUS_ROCKET","SLOT_ITEM.LAND_MATTRESS_ROCKET_MARKER","SLOT_ITEM.LEE_ENFIELD_RIFLE_GRENADE_SLOT_ITEM_MP","SLOT_ITEM.LIEUTENANT_GARRISON_ITEM","SLOT_ITEM.LIGHT_AT_MINE_RECENTLY_HIT_HEAVY_VEHICLE","SLOT_ITEM.LIGHT_AT_MINE_RECENTLY_HIT_LIGHT_VEHICLE","SLOT_ITEM.M01_CONSCRIPT_MOSIN_NAGANT","SLOT_ITEM.M15A1_AA_MODE_ACTIVATED","SLOT_ITEM.M15A1_AA_MODE_ACTIVATED_LEFT","SLOT_ITEM.M15A1_AA_MODE_ACTIVATED_MAIN_GUN","SLOT_ITEM.M17_RIFLE_GRENADE_SLOT_ITEM_MP","SLOT_ITEM.M1919A6_LMG_ICON_DUMMY","SLOT_ITEM.M1C_GARAND","SLOT_ITEM.M1C_PATHFINDER_GARAND","SLOT_ITEM.M23_SMOKE_STREAM_GRENADE_ANTI_TANK_ITEM_MP","SLOT_ITEM.M23_SMOKE_STREAM_GRENADE_ITEM_MP","SLOT_ITEM.M24_ANTI_TANK_GRENADIER_GRENADE","SLOT_ITEM.M2HB_50CAL_SHERMAN","SLOT_ITEM.M2HB_TURRET_MOUNTED_M8_MP","SLOT_ITEM.M2HB_TURRET_MOUNTED_SHERMAN_MP","SLOT_ITEM.M5_STUART_DAMAGE_ENGINE_SHOT_SLOT_ITEM_MP","SLOT_ITEM.M5_STUART_SHELL_SHOCK_SHOT_SLOT_ITEM_MP","SLOT_ITEM.M8_CANISTER_SHOT_SLOT_ITEM_MP","SLOT_ITEM.M8_GREYHOUND_RECON_ACTIVATED","SLOT_ITEM.MAJOR_GARRISON_ITEM","SLOT_ITEM.MG34_PINTLE_HETZER","SLOT_ITEM.MG42_TURRET_MOUNTED_BRUMMBAR","SLOT_ITEM.MG42_TURRET_MOUNTED_BRUMMBAR_MP","SLOT_ITEM.MG42_TURRET_MOUNTED_KING_TIGER_MP","SLOT_ITEM.MG42_TURRET_MOUNTED_PANTHER","SLOT_ITEM.MG42_TURRET_MOUNTED_PANTHER_MP","SLOT_ITEM.MG42_TURRET_MOUNTED_PANTHER_WG_MP","SLOT_ITEM.MG42_TURRET_MOUNTED_PZIV","SLOT_ITEM.MG42_TURRET_MOUNTED_PZIV_MP","SLOT_ITEM.MG42_TURRET_MOUNTED_STUGIV","SLOT_ITEM.MG42_TURRET_MOUNTED_STUGIV_MP","SLOT_ITEM.MG42_TURRET_MOUNTED_TIGER","SLOT_ITEM.MG42_TURRET_MOUNTED_TIGER_MP","SLOT_ITEM.MG42_TURRET_MOUNTED_TIGER_TOW","SLOT_ITEM.MINESWEEPER","SLOT_ITEM.MORTAR_FLARE_MP","SLOT_ITEM.MOSIN_NAGANT_SNIPER_RIFLE_ITEM","SLOT_ITEM.MOSIN_NAGANT_SNIPER_RIFLE_ITEM_MP","SLOT_ITEM.OBERSOLDATEN_MG34_LMG_MOVING_MP","SLOT_ITEM.OBERSOLDATEN_MG34_LMG_MOVING_NO_PRONE_MP","SLOT_ITEM.OBERSOLDATEN_MP44_INFARED","SLOT_ITEM.OPEL_SUPPLY_SLOT_ITEM","SLOT_ITEM.PAK40_CRITICAL_SHOT_MP","SLOT_ITEM.PAK43_CRITICAL_SHOT_MP","SLOT_ITEM.PANZER_GRENADIER_MP44_ITEM","SLOT_ITEM.PANZER_GRENADIER_MP44_ITEM_MP","SLOT_ITEM.PANZERBUSCHE_39","SLOT_ITEM.PANZERBUSCHE_39_MP","SLOT_ITEM.PANZERFUISILIER_FLARE_MP","SLOT_ITEM.PANZERFUSILIER_AT_RIFLE_GRENADE","SLOT_ITEM.PANZERFUSILIER_G43","SLOT_ITEM.PANZERFUSILIER_GRENADE","SLOT_ITEM.PANZERSHRECK","SLOT_ITEM.PANZERSHRECK_AT_WEAPON_ITEM","SLOT_ITEM.PANZERSHRECK_DESTROY_ENGINE","SLOT_ITEM.PANZERSHRECK_MP","SLOT_ITEM.PANZERSHRECK_SLOT1","SLOT_ITEM.PANZERSHRECK_SLOT1_MP","SLOT_ITEM.PANZERSHRECK_SLOT2","SLOT_ITEM.PANZERSHRECK_SLOT2_MP","SLOT_ITEM.PARADROP_REINFORCE_ITEM","SLOT_ITEM.PARATROOPER_M1919A6_LMG_MOVING_NO_PRONE_MP","SLOT_ITEM.PARATROOPER_M1919A6_LMG_MP","SLOT_ITEM.PARATROOPER_MK2_GRENADE_MP","SLOT_ITEM.PARATROOPER_THOMPSON_DUMMY","SLOT_ITEM.PARATROOPER_THOMPSON_MP","SLOT_ITEM.PARTISAN_DP_28_LIGHT_MACHINE_GUN_PACKAGE_MP","SLOT_ITEM.PARTISAN_MG42_LMG_MP","SLOT_ITEM.PATHFINDERS_SNIPER_ITEM","SLOT_ITEM.PENAL_TROOP_SATCHEL_CHARGE_ITEM_MP","SLOT_ITEM.PERSHING_HVAP_PIERCING_ITEM_MP","SLOT_ITEM.PIAT_SPIGOT_MORTAR_MP","SLOT_ITEM.PIONEER_FLAMETHROWER","SLOT_ITEM.PIONEER_FLAMETHROWER_ABILITY","SLOT_ITEM.PIONEER_FLAMETHROWER_ABILITY_MP","SLOT_ITEM.PIONEER_FLAMETHROWER_MP","SLOT_ITEM.PIONEER_STUN_GRENADE_MP","SLOT_ITEM.PM_AEF_OFFENSIVE_PUNCH_ITEM","SLOT_ITEM.PPSH41_ASSAULT_PACKAGE","SLOT_ITEM.PPSH41_ASSAULT_PACKAGE_DUMMY_ITEM_MP","SLOT_ITEM.PPSH41_ASSAULT_PACKAGE_MP","SLOT_ITEM.PTRS_41_ANTI_TANK_RIFLE_CONSCRIPT_MP","SLOT_ITEM.PTRS_41_ANTI_TANK_RIFLE_GUARD_TROOP","SLOT_ITEM.PTRS_41_ANTI_TANK_RIFLE_GUARD_TROOP_ASSAULT_MP","SLOT_ITEM.PTRS_41_ANTI_TANK_RIFLE_GUARD_TROOP_MP","SLOT_ITEM.PTRS_41_ANTI_TANK_RIFLE_PARTISAN_TROOP_MP","SLOT_ITEM.PUMA_AIMED_SHOT_MP","SLOT_ITEM.PUMA_CRITICAL_SHOT_MP","SLOT_ITEM.RANGER_PANZERSHRECK_MP","SLOT_ITEM.REAR_ECHELON_RIFLE_GRENADE_ACTIVATED","SLOT_ITEM.REAR_ECHELON_RIFLE_VOLLEY_FIRE","SLOT_ITEM.RECOUP_ACTIVE","SLOT_ITEM.RGD_1_SMOKE_GRENADE_ITEM","SLOT_ITEM.RGD_1_SMOKE_GRENADE_ITEM_MP","SLOT_ITEM.RGD_33_SLEEVED_GRENADE_ITEM","SLOT_ITEM.RGD_33_SLEEVED_GRENADE_ITEM_LONGTIMER","SLOT_ITEM.RGD_33_SLEEVED_GRENADE_ITEM_MP","SLOT_ITEM.RIFLEMAN_AT_RIFLE_GRENADE","SLOT_ITEM.RIFLEMEN_30_CAL","SLOT_ITEM.RIFLEMEN_FLARE","SLOT_ITEM.RIFLEMEN_M1918_BAR_MP","SLOT_ITEM.RIFLEMEN_MK2_GRENADE_MP","SLOT_ITEM.RIFLEMEN_TRAINING_DUMMY_CARBINE","SLOT_ITEM.RIFLEMEN_TRAINING_SATCHEL_ITEM","SLOT_ITEM.ROKS_2_FLAMETHROWER_ITEM","SLOT_ITEM.ROKS_2_FLAMETHROWER_ITEM_MP","SLOT_ITEM.RPG_40_ANTI_TANK_GRENADE_MP","SLOT_ITEM.RPG_43_ANTI_TANK_GRENADE","SLOT_ITEM.RPG_43_ANTI_TANK_GRENADE_MP","SLOT_ITEM.SAPPER_BREN_LIGHT_MACHINE_GUN_MP","SLOT_ITEM.SAPPER_STUN_GRENADE_MP","SLOT_ITEM.SAPPER_VICKERS_K_LIGHT_MACHINE_GUN_MP","SLOT_ITEM.SATCHEL_CHARGE_ITEM_MP","SLOT_ITEM.SELF_REPAIR_DUMMY_SLOT_ITEM","SLOT_ITEM.SHERMAN_BATTLE_GROUP_ITEM_MP","SLOT_ITEM.SHOCK_TROOP_RG_42_GRENADE","SLOT_ITEM.SHOCK_TROOP_RG_42_GRENADE_MP","SLOT_ITEM.SIPHON_ACTIVE","SLOT_ITEM.SNIPER_FLARE_MP","SLOT_ITEM.SNIPER_RIFLE_ITEM","SLOT_ITEM.SNIPER_RIFLE_ITEM_MP","SLOT_ITEM.SNIPER_SMOKE_MARKER_GRENADE_MP","SLOT_ITEM.SNIPER_SUPPRESSIVE_VOLLEY_MP","SLOT_ITEM.SOVIET_FLAG","SLOT_ITEM.SPEARHEAD_ITEM","SLOT_ITEM.STALK_ITEM","SLOT_ITEM.STORMTROOPER_MP44_MP","SLOT_ITEM.STUG_CRITICAL_SHOT_MP","SLOT_ITEM.STUG_ELEFANT_PAK40_PAK43_BRUMMBAR_CRITICAL_SHOT","SLOT_ITEM.STUG_ELEFANT_PAK40_PAK43_BRUMMBAR_CRITICAL_SHOT_MP","SLOT_ITEM.STURMTIGER_RELOAD_ACTIVE","SLOT_ITEM.SU76M_HE_ROUND_ITEM","SLOT_ITEM.SU76M_HE_ROUND_ITEM_MP","SLOT_ITEM.SUPPORT_SQUAD_SETUP","SLOT_ITEM.SUPPRESS_FIRE_ITEM","SLOT_ITEM.SWS_LOCKDOWN_SETUP","SLOT_ITEM.TANK_HUNTER_SHOCK_BAZOOKA_VET","SLOT_ITEM.TIGER_ACE_CRITICAL_SHOT_MP","SLOT_ITEM.TIGER_FLARE_TOW","SLOT_ITEM.TOMMY_BREN_LIGHT_MACHINE_GUN_MP","SLOT_ITEM.TOMMY_FLAMETHROWER","SLOT_ITEM.TOMMY_GAMMON_BOMB_HEAVY","SLOT_ITEM.TOMMY_GAMMON_BOMB_MEDIUM","SLOT_ITEM.TOMMY_HEAT_GRENADE","SLOT_ITEM.TOMMY_MILLS_BOMB","SLOT_ITEM.TOMMY_MILLS_BOMB_ASSAULT","SLOT_ITEM.TOMMY_OFFICER_SMOKE_MARKER_GRENADE_MP","SLOT_ITEM.TOMMY_SCOPED_RIFLE_ITEM_MP","SLOT_ITEM.TOMMY_STEN_SMG","SLOT_ITEM.TROOP_SUPPORT_DUMMY_MEDIC","SLOT_ITEM.UNIVERSAL_CARRIER_VICKERS_K_PACKAGE_MP","SLOT_ITEM.UNIVERSAL_CARRIER_VICKERS_MMG_SUPPRESSIVE_MP","SLOT_ITEM.URBAN_ASSAULT_FLAMETHROWER_MP","SLOT_ITEM.URBAN_ASSAULT_SATCHEL_CHARGE_ITEM_MP","SLOT_ITEM.VALENTINE_SMOKE_MARKER_GRENADE_MP","SLOT_ITEM.VICKERS_K_LIGHT_MACHINE_GUN_MP","SLOT_ITEM.VOLKSGRENADIER_FIRE_GRENADE_MP","SLOT_ITEM.VOLKSGRENADIER_GRENADE_MP","SLOT_ITEM.VOLKSGRENADIER_MP44_ITEM_MP","SLOT_ITEM.VOLKSGRENADIER_PANZERFAUST_MP","SLOT_ITEM.VOLKSGRENADIER_PANZERFAUST_VET_4_MP","SLOT_ITEM.WAFFEN_BUNDLED_ASSAULT_GRENADE","SLOT_ITEM.WEST_GERMAN_MINESWEEPER","SLOT_ITEM.WG_BLENDKORPER_SMOKE_UI_ITEM","SLOT_ITEM.WG_PANZER_IV_ARMORED_SKIRTS","CRIT._NO_CRITICAL","CRIT._NO_CRITICAL_MINE","CRIT._NO_CRITICAL_REAR","CRIT._SP_ANIA_EXPLOSIVE","CRIT._SP_ANIA_KILLED","CRIT.ASSAULT_MODIFIERS","CRIT.ATTACK_PLAN_MODIFIERS","CRIT.AXIS_ASSAULT_MODIFIERS","CRIT.BRIDGE_DEMOLITION_MAKE_WRECK","CRIT.BRIDGE_MAKE_WRECK","CRIT.BUILDING_ABANDON","CRIT.BUILDING_BRACED","CRIT.BUILDING_DESTROY","CRIT.BUILDING_DESTROY_CONSTRUCTION","CRIT.BUILDING_DESTROY_SUPPLY_CENTER","CRIT.BUILDING_FIRE_DAMAGE_DOT","CRIT.BUILDING_FIRE_DAMAGE_PANEL","CRIT.BUILDING_PANEL_DAMAGE_CRITICAL","CRIT.BUILDING_RED_BUILD_TIME_INCREASE","CRIT.BUILDING_STRONG_CRITICAL","CRIT.BUILDING_WEAK_CRITICAL","CRIT.BUILDING_YELLOW_BUILD_TIME_INCREASE","CRIT.BULLET_HIT_CRITICAL","CRIT.BURN","CRIT.BURN_DEATH","CRIT.BURN_DEATH_OUT_OF_CONTROL","CRIT.BURN_WORLD_OBJECT","CRIT.BURN_WORLD_OBJECT_DEATH","CRIT.CAMOUFLAGE_MINE","CRIT.CHURCHILL_TANK_SHOCK_MODIFIERS","CRIT.DETONATE_BANGALORE","CRIT.DETONATE_DEMOLITION_CHARGE","CRIT.DETONATE_MINE","CRIT.EMPLACEMENT_EMPTY","CRIT.EMPLACEMENT_FLAME_CRITICAL","CRIT.EMPLACEMENT_KILL_LOADER","CRIT.EXPLOSIVE_DESTROY","CRIT.GOLIATH_DESTROY","CRIT.HEROIC_CHARGE_FATIGUE","CRIT.MAKE_CASUALTY","CRIT.SOLDIER_BLIND","CRIT.SOLDIER_EXECUTED","CRIT.SOLDIER_EXPLOSIVE_ROUND","CRIT.SOLDIER_FLAMETHROWER_EXPLODE","CRIT.SOLDIER_FORCE_RETREAT","CRIT.SOLDIER_FROZEN","CRIT.SOLDIER_KILLED","CRIT.SOLDIER_KILLED_DEATH_INTENSITY_100","CRIT.SOLDIER_KILLED_DEATH_INTENSITY_30","CRIT.SOLDIER_KILLED_DEATH_INTENSITY_60","CRIT.SOLDIER_KILLED_HMG_DEATH","CRIT.SOLDIER_PIN","CRIT.SOLDIER_SLOW","CRIT.SOLDIER_SNIPED","CRIT.SOLDIER_SNIPED_IN_HALFTRACK","CRIT.SOLDIER_SNIPED_MAKE_CASUALTY","CRIT.SOLDIER_SNIPED_STILL_ALIVE","CRIT.SOLDIER_STUN","CRIT.SOLDIER_SUPPRESS","CRIT.SQUAD_ITEM_DAMAGED","CRIT.STUNNED_CANNOT_SHOOT_10_SECONDS","CRIT.STUNNED_CANNOT_SHOOT_MOVE_10_SECONDS","CRIT.SUPPLY_DROP_BLOW_UP","CRIT.TANK_TRAP_DESTROY","CRIT.TEAM_WEAPON_DISABLING_SHOT","CRIT.VEHICLE_ABANDON","CRIT.VEHICLE_ABANDON_STURMTIGER","CRIT.VEHICLE_AEC_TEMP_ENGINE_DAMAGE","CRIT.VEHICLE_AEC_TEMP_IMMOBILITY","CRIT.VEHICLE_BLIND","CRIT.VEHICLE_CREW_DAZED_JAGDTIGER","CRIT.VEHICLE_CREW_SHOCKED","CRIT.VEHICLE_CREW_STUNNED","CRIT.VEHICLE_CREW_STUNNED_2","CRIT.VEHICLE_DAMAGE_ENGINE","CRIT.VEHICLE_DAMAGE_ENGINE_INCREMENTAL","CRIT.VEHICLE_DAMAGE_ENGINE_REAR","CRIT.VEHICLE_DAMAGE_ENGINE_REAR_RAMMING","CRIT.VEHICLE_DAMAGE_ENGINE_SNARE","CRIT.VEHICLE_DECREW","CRIT.VEHICLE_DESTROY","CRIT.VEHICLE_DESTROY_BREW_UP","CRIT.VEHICLE_DESTROY_ENGINE","CRIT.VEHICLE_DESTROY_ENGINE_REAR","CRIT.VEHICLE_DESTROY_MAINGUN","CRIT.VEHICLE_DESTROY_MAINGUN_RAMMING","CRIT.VEHICLE_DESTROY_QUAD_50","CRIT.VEHICLE_DESTROY_SEARCHLIGHT_IR_HALFTRACK","CRIT.VEHICLE_DESTROY_WEAPON_TEAM","CRIT.VEHICLE_DRIVER_INJURED","CRIT.VEHICLE_ENGINE_BURNING","CRIT.VEHICLE_EXHAUST_DAMAGED","CRIT.VEHICLE_GUNNER_INJURED","CRIT.VEHICLE_KILL_BRIT_TANK_COMMANDER","CRIT.VEHICLE_KILL_COMMANDER","CRIT.VEHICLE_KILL_DRIVER_RUSSIAN","CRIT.VEHICLE_KILL_GUNNER_RUSSIAN","CRIT.VEHICLE_KILL_RELOADER_RUSSIAN","CRIT.VEHICLE_KILL_TOP_GUNNER_HARDPOINT_1","CRIT.VEHICLE_KILL_TOP_GUNNER_HARDPOINT_2","CRIT.VEHICLE_KILL_TOP_GUNNER_HARDPOINT_4","CRIT.VEHICLE_LIGHT_DAMAGE_ENGINE","CRIT.VEHICLE_LIGHT_DAMAGE_ENGINE_REAR","CRIT.VEHICLE_LIGHT_DESTROY_ENGINE","CRIT.VEHICLE_LIGHT_DESTROY_ENGINE_REAR","CRIT.VEHICLE_LOADER_INJURED","CRIT.VEHICLE_LOSE_TREADS_OR_WHEELS","CRIT.VEHICLE_MAKE_WRECK","CRIT.VEHICLE_OPTICS_DAMAGED","CRIT.VEHICLE_OPTICS_DAMAGED_TEMP","CRIT.VEHICLE_OUT_OF_CONTROL_FAST","CRIT.VEHICLE_OUT_OF_CONTROL_SLOW","CRIT.VEHICLE_OUT_OF_FUEL_GERMAN_SP","CRIT.VEHICLE_SHELL_SHOCKED","CRIT.VEHICLE_SNIPER_SLOW","CRIT.VEHICLE_STUCK_IN_MUD","CRIT.VEHICLE_TANK_GRAB_ABANDON_SP","CRIT.VEHICLE_TEMP_IMMOBILITY","CRIT.VEHICLE_TURRET_DISABLED_TEMP","CRIT.VEHICLE_UNIVERSAL_CARRIER_FLAMETHROWER_EXPLODE","CRIT.VEHICLE_VISION","CRIT.VEHICLE_VISON_BLOCK_DAMAGED","CRIT.VEHICLE_WEAPON_DISABLED_TEMP","CRIT.WORLD_DESTROY_BARRIER","CRIT.WORLD_OBJECT_DESTROY","CRIT.WORLD_OWNED_VEHICLE_ABANDON","BridgeReplace_OnInit","SkinPreviewCapture_Init","SkinPreviewCapture_SpawnVehicles","SkinPreviewCapture_UIInit","SkinPreviewCapture_CycleAndCaptureScreenshots","SkinPreviewCapture_Begin","SkinPreviewCapture_BeginCountdown","SkinPreviewCapture_ExitCountdown","SkinPreviewCapture_Exit","SkinPreviewCapture_StartCountdown","Map_PreInit","SkinPreviewCapture_Configure","AOH_PreInit","AV_PreInit","AV_Init","AV_UpdateObjectiveTimer","AV_UIInit","AV_End","CCM_ActionSpawnUKFSpawner","CCM_ActionSpawnUKFMiscSpawner","Squad_ToClipboardData","Squad_FromClipboardData","Entity_FromClipboardData","Entity_ToDataParameters","Entity_GetHealthPointsString","Squad_ToDataParameters","Squad_GetHealthPointsString","Player_ToDataParameters","Player_GetSetting","Player_SetSetting","Player_GetSettings","LocalPlayer_GetSettings","Data_GetHealthModifiedString","Data_GetOwnerChangedString","CCM_EventCueClickManger","CCM_EventMessage","CCM_EventKickerMessage","CCM_EventKickerMessageEval","CCM_EventKickerHealthMessageEval","CCM_ErrorMessage","Item_GetEnemyPlayer","CCM_SpawnQueueTick","CCM_SpawnQueueInit","CCM_SpawnQueueAdd","CCM_DummyMessage","CCM_PlayerCommandIssued","CCM_SquadCommandIssued","CCM_EntityCommandIssued","CCM_CustomUIEvent","Variable_FromG","Ternary","CCM_PlayerCommandIssued2","CCM_ConfigInit","__subMenu_SetUpdateRate","__subMenu_SpawnUnits","__subMenu_ManipulateSquadMembers","__panel_SelectionHealth","TestFormAdd","TestFormRender","Test_SlotItemRemoveSpam","Test_SlotItemRemoveSpam_Tick","CCM_HealthMonitor_Tick","CCM_HealthMonitor_HandleHealthMessage","CCM_HealthMonitor_RegisterNewItem","CCM_HealthMonitorInit","CCM_SuppressionMonitor_Tick","CCM_SuppressionMonitor_HandleMessage","CCM_SuppressionhMonitor_RegisterNewItem","CCM_SuppressionMonitorInit","CCM_Init","CCM_UIInit","CCM_BroadcastMessageReceived","CCM_Broadcast","CCM_ShowCrosshair","CCM_HideCrosshair","CCM_DisableUI","CCM_EnableUI","CCM_KillSelection","CCM_DeleteSelection","CCM_KillSquad","CCM_DeleteSquad","CCM_KillEntity","CCM_DeleteEntity","CCM_EnableFOW","CCM_DisableFOW","CCM_EnableAI","CCM_DisableAI","CCM_SetAIDifficulty","CCM_SetSelectionHealth","CCM_AddSelectionHealthPercentage","CCM_AddSelectionHealthPoints","CCM_SetSelectionInvulnerability","CCM_SetSelectionOwner","CCM_AddResource","CCM_ResetResource","CCM_AddPopulationCap","CCM_SetInstantProductionEnabled","CCM_SetInstantConstructionEnabled","CCM_SetInstantAbilityRechargeEnabled","CCM_SpawnSquad","CCM_SpawnEntity","CCM_SpawnSlotItem","CCM_IncreaseSelectionXP","CCM_IncreaseSelectionVeterancyLevel","CCM_InstantReinforceSelection","CCM_SplitSelection","CCM_RemoveSelectionCriticals","CCM_RemoveSquadCritical","CCM_RemoveEntityCritical","CCM_ApplyCriticalToSelection","CCM_SetSquadAutoTargetting","CCM_RemoveSquadUpgrade","CCM_RemoveEntityUpgrade","CCM_RemoveSquadSlotItem","CCM_SetSelectionFacing","CCM_TeleportSelection","CCM_KillEverything","CCM_DeleteEverything","CCM_RotateEntity","CCM_SetHealthMonitorEnabled","CCM_SetSuppressionMonitorEnabled","CCM_SelectedTeamWeaponGarrisonFacePosition","CCM_CancelTeamWeaponGarrisonFacingOrder","CCM_AddSelectionSuppression","CCM_SetAllAIPlayersEnabled","CCM_ResetSelectionVeterancy","CCM_SetResourceIncomeEnabled","CCM_SetHealthMonitorUpdateRate","CCM_SetSuppressionMonitorUpdateRate","CCM_UnlockCommanderAbility","CCM_ClearCommanderAbilities","CCM_ModifySquadMovementSpeed","CCM_SetSelectionOwnerToEnemy","CCM_SquadToEntity","CCM_SetEntityAnimatorState","CCM_SetSquadAnimatorState","CCM_SetSelectionAnimatorState","CCM_SetSelectionSkinType","CCM_DropSelectionWeapons","CCM_CaptureAllTerritorySectors","CCM_NeutralizeAllTerritorySectors","CCM_SquadToSkinPreviewEntity","Enhanced_Init","Enhanced_SystemInit","Enhanced_BroadcastMessageReceived","Enhanced_UITick","Enhanced_SetButtonsEnabled","Enhanced_ResetButtonIcons","Enhanced_SetButtonsVisible","Enhanced_PreInit","Enhanced_UIInit","Dude","MyMap_OnInit","MyMap_BonusUnitKilled","prnt","toCharArray","export","include","getBlueprintIfItExists","getBlueprintName","instanceOf","parent","Loc_Create","broadcastMessage","delayedStart","Map_PlayerBonusUnitKilled","MyFunction","Map_OnInit","Gardeners_PreInit","AutoAbandonManager","AutoAbandon_Add","AutoAbandon_Remove","AutoDeleteManager","AutoDelete_Add","AutoDelete_Remove","AutoRetreatManager","AutoRetreat_Add","AutoRetreat_Remove","Parameters_ToStringData","Parameters_FromStringData","Player_FromStringData","Squad_FromStringData","Entity_FromStringData","Broadcast","Camera_MoveToCallback","Camera_MoveToCallback_Tick","CameraPosition","Class","Color","Margin","Padding","Player","Control","Button_CreateConfig","Button_GetIcon","Button","FormControl_Init","FormControl_Refresh","Form","Icon","Label","NumericUpDown_CreateIconConfig","NumericUpDownScroll_Tick","NumericUpDown_RegisterAutoScroll","NumericUpDown_UnregisterAutoScroll","NumericUpDown","MenuControl_Init","Menu_AutoRefresh","Menu_AutoCheckEnabledScan","Menu_AutoCheckCheckedScan","CloseMenus","Menu","Menu_CreateBorderImage","Panel_GetMultipartBackground","Panel","PanelColumn","PanelColumnCollection","Class_GetUniqueID","Class_CreateInstance","Construct","CompanyCommander_Create","ControlSystem_Init","Button_FromTag","ButtonCallbackHandler","Control_GetName","Control_GetX","Control_GetY","Control_GetPath","Control_GetText","Control_GetTag","BPData_GetExtensions","Loc_Get","EBP_HasExtension","EBPData_HasExtension","EBP_GetScreenName","EBP_GetIcon","EBPData_GetUIExt","EBPData_GetScreenName","EBPData_GetIcon","SBP_GetScreenName","SBP_GetIcon","SBPData_GetRaceUIExt","SBPData_GetScreenName","SBPData_GetIcon","Crit_GetScreenName","CritData_GetUIExt","CritData_GetScreenName","CritData_GetIcon","UPG_GetScreenName","UPGData_GetScreenName","SlotItem_GetScreenName","SlotItemData_GetScreenName","EGroup_ToTable","EGroup_IsAlive","EGroup_IsCapturedByTeam2","LocalImport","ImportSystem","ImportDataTables","Library_Load","Lib_EnableMessages","Lib_SetMessagesEnabled","Lib_SetupMod","Mod_GetIcon","Mod_GetAbilityBlueprint","Mod_GetSquadBlueprint","Mod_GetEntityBlueprint","Mod_GetUpgradeBlueprint","Msg_Pos","Msg_3D","Lib_GameOver","Debug_SetMessagesEnabled","TryCatch","Library_Setup","Entity_Validate","Entity_AddHealthPercentage","Entity_AddHealthPoints","Entity_GetOwnerString","Entity_GetBPName","Entity_GetCriticals","Entity_RemoveCriticals","Entity_IsTeamWeapon","Entity_IsValidSafe","Entity_GetUpgrades","Entity_Rotate","Entity_GetTypes","Entity_IsOfType2","Entity_HasUpgrades","EBP_GetTypes","EBP_IsOfType","Entity_Decrew","Entity_PrepareForScreenshot","Entity_SetSkinSeason","Entity_GetBlueprintName","Entity_HasModifierExt","Percentage_Normalize","Round","scientific","Player_GetIndex","Player_SetResourcesEnabled","Player_ResetResources","Player_GetAllSquads","Player_DestroyAllSquads","Player_GetDisplayRaceName","Player_GetDisplayRaceNameLong","Player_GetNameWithFaction","AIDifficulty_Tostring","Player_IsAI","Player_ForEachSquad","Pos_NormalizeHeight","Rule_AddIntervalAndRun","Rule_AddIfNotExists","Rule_AddIntervalIfNotExists","Rule_ChangeIntervalIfExists","Rule_AddDelayed","Rule_AddDelayedIfNotExists","Modify_SetSquadtAutoTargetting","Modify_SetEntityAutoTargetting","Modify_SetEntityAutoTargettingAllHardpoints","Modify_SetSquadAutoTargettingAllHardpoints","Modify_SetSGroupAutoTargettingAllHardpoints","Modify_SetEGroupAutoTargettingAllHardpoints","Modify_SquadTypeEnableCapturing","Selection_UnselectAll","Selection_IsOneEntity","Selection_IsOneSquad","Selection_IsOneSquadOrOneEntity","Selection_IsOneOrMoreSquads","Selection_IsOneOrMoreEntities","Selection_IsSquadsOrEntities","Selection_IsSquadOrEntity","Selection_GetSquad","Selection_GetEntity","Selection_GetSquads","Selection_GetEntities","Misc_SomethingIsSelected","Selection_ForEachSquad","Selection_ForEachEntity","Selection_IsNotInvulnerable","Selection_IsNotNeutralSquadOrEntity","Selection_CountInvulnerables","Selection_IsInvulnerable","SelectionMonitor_Init","SelectionMonitor","SelectionMonitor_GetID","SelectionMonitor_AddSquad","SelectionMonitor_AddEntity","SelectionMonitor_RemoveSquad","SelectionMonitor_RemoveEntity","SelectionMonitor_RemoveItemListener","SelectionMonitor_RemoveSquadLister","SelectionSystem_RemoveEntityLister","SGroup_ToTable","SGroup_SetPosition","SGroup_CountEntities","SGroup_IsAlive2","Squad_CountSpawned","Squad_IsPlane","Squad_SetSelectable","Squad_GetLastAttackerSquad","Squad_IsSelected","Squad_AddHealthPercentage","Squad_AddHealthPoints","Squad_Abandon","Squad_GetOwnerString","Squad_GetBPName","Squad_GetCriticals","Squad_RemoveCriticals","Squad_IsValidSafe","Squad_SetAutoTargetting","__RegisterSquadAutoTargettingModifier","__UnRegisterSquadAutoTargettingModifier","__RegisterEntityAutoTargettingModifier","__UnRegisterEntityAutoTargettingModifier","Squad_SetAllAutoTargetting","Squad_GetAutoTargetting","Squad_GetAllAutoTargetting","Squad_GetUpgrades","Squad_HasUpgrades","Squad_RemoveSlotItem","Squad_RemoveUpgradeFully","Squad_ForEachHeldSquad","Squad_DestroyHeldSquads","Squad_KillHeldSquads","Squad_ModifyVehicleSpeed","Squad_ModifyVehicleRotationSpeed","Squad_ModifyTurretHorizontalSpeed","Squad_ModifyMovementSpeed","Squad_GetTypes","Squad_IsOfType","SBP_GetTypes","SBP_IsOfType","Squad_GetEntityTable","Squad_ToEntities","Squad_ToEntity","Squad_AddMainGunHorizontalRotation","Squad_SetMainGunHorizontalRotation","Squad_Decrew","Squad_SetSkinSeason","Squad_RemoveSlotItems","Squad_GetEntityStateString","Squad_RemoveUpgradeIfPresent","Squad_RemoveUpgradesIfPResent","String_Match","String_Replace","String_AddGenetive","String_Split","Number_TrailingZeroes","Outpost","Outpost_Init","OutpostManager_Register","OutpostManager_Tick","OutpostPatrol","OutpostCaptureTrigger","OutpostPatrolAlarmedSquads_Register","OutpostPatrolAlarmedSquads_Tick","OutpostPatrolManager_Register","OutpostPatrolManager_Tick","OutpostRadioPost","OutpostRadioPostManager_Register","OutpostRadioPostManager_Tick","Table_AddTable","Table_GetSmallest","Table_GetLargest","Table_RemoveValue","Table_IsEmpty","Table_GetRandomBlueprint","Table_Remove","Table_ToIndexableList","Table_Count","Table_Compare","RangeTable_GetRandomValue","OutpostReinforcementsManager_Register","OutpostReinforcementsManager_Tick","Team_GetRandomPlayer","Team_GetRandomPlayers","Time_TicksToSeconds","Time_SecondsToTicks","Time_MinutesToTicks","UI_EnableSelectionVisuals","UI_EnableSquadSelectionVisuals","SelectionVisual_Tick","SelectionVisual_RegisterEntity","UI_ScalePoint","Util_DelaySeconds","Util_DelayMinutes","Util_DelayRandom","Util_DelayRandomSeconds","Util_GetRandomPosExtended","Util_GetRandomHeadingPos","UIFrame_Destroy","Misc_Tester","Util_DistanceFromLine","Util_DistancePointToTeamShortest","HintPoints_Remove","MapIcon_CreateAndFacePosition","toboolean","ResourceType_ToString","ResourceType_FromString","ResourceType_ToDisplayString","Selection_GetPlayer","Objective_UpdateTitle","Objective_StartLocally","Util_CallFunctionsWithParameters","World_ForEachEntity","World_DivideTerritoryBetweenTeams","World_GetWidthRange","World_GetLengthRange","World_RegisterPlayers","World_GetEverythingNearPoint","World_GetAll","World_GetAllSquads","World_ForEeachSquad","World_OneOrMoreAIPlayerIsEnabled","World_OneOrMoreAIPlayerIsDisabled","World_CleanUpTheDeadAll","World_GetAllTerritoryPointEntities","Villagers_PreInit","WarDrive_Init","_getPlayerMineEBP","_spawnMines","WarDrive_GetPlayerReconAbility","WarDrive_ReconSweepBetweenTeams","WarDrive_GetNextEffectDelay","WarDrive_PickRandomEffect","WarDrive_SplitTimeUnits","WarDrive_FormatTime","WarDrive_Monitor","Team_HasTerritoryPoint","WarDrive_EntityKilled","WarDrive_SquadKilled","TestEffect","WarDrive_EnableEffect","WarDrive_RemoveModifiers","WarDrive_GetIcon","WarDrive_RegisterModifier","Modify_SquadBuildTime","Modify_SquadReinforceTime","Modify_EntityCaptureTime","WarDrive_ObjectiveInit","WarDrive_ObjectiveAfterInt","WarDrive_Pager","WarDrive_AbilityExecuted","WarDrive_GetAbilityBlueprint","WarDrive_PreInit","CCM_AddInfiniteResourcesPopcap","CCM_ActionKillSelection","CCM_ActionDeleteSelection","CCM_ActionTeleportSelection","CCM_ActionIncreaseSelectionVeterancy","CCM_ActionIncreaseSelectionHealth","CCM_ActionDecreaseSelectionHealth","CCM_ModifySelectionHealth","CCM_ActionAbandonSelected","CCM_ActionRemoveCriticals","CCM_ActionDropSlotItems","CCM_ActionInstantReinforce","CCM_ActionAddPreciseManpower","CCM_ActionAddPreciseFuel","CCM_ActionAddPreciseMunition","_CCM_SpawnSpawnerSquad","CCM_ActionSpawnSovietSpawner","CCM_ActionSpawnAEFSpawner","CCM_ActionSpawnGermanSpawner","CCM_ActionSpawnWestGermanSpawner","CCM_ActionAddFullHealth","CCM_ActionKillOneEntity","CCM_ActionDeleteOneEntity","CCM_ActionSpawnGermanMiscSpawner","CCM_ActionSpawnSovietMiscSpawner","CCM_ActionSpawnWestGermanMiscSpawner","CCM_ActionSpawnAEFMiscSpawner","CCM_DataInit","CCM_CopySelection","CCM_PasteSelection","Clipboard_Clear","CCM_PreInit","CCM_SystemInit","CCM_PlayerResetAbilities","Player_GetSettingsKey","CCM_PlayerAbilityCompleteListener","CCM_PlayerAbilityListener","CCM_RegisterPlayerAction","Squad_GetSpawnerRaceIndex","Squad_GetSpawnerTable","Squad_GetSpawnAbilityPrefix","CCM_SquadAbilityListener","CCM_CountSpawnTableItems","_CCM_InitSpawnerSquad","CCM_AutoHideAbilities","Entity_CreateAndSpawnTowardTeamWeapon","Entity_GetUpgradeTable","Entity_ApplyCriticalHit","Entity_GetText","Entity_Abandon","EntityBP_IsBuilding","Util_Destroy","Util_GetBPName","Misc_CheckForParentSquad","Squad_GetTableKey","Entity_GetTableKey","Util_GetTablekey","Util_SetInvulnerable","Player_GetIDSafe","Table_ForEach","CCM_Msg","CCM_ClearMSG","CCM_GetAbilityBleprint","CCM_GetSquadBlueprint","CCM_GetEntityBlueprint","CCM_GetUpgradeBlueprint","CCM_GetIcon","CCM_EventCue","Misc_AddSpawnedItemToSystem","Util_AddHealth","Misc_DoPercentageSum","Util_SetPosition","Util_GetGameID","Util_DecodeGameID","Misc_SpawnSlotItemOnGround","Squad_ModifySpeed","Squad_GetHealthTable","Squad_ApplyHealthTable","Squad_GetPlayerOwnerSafe","Squad_GetHeadingTable","Squad_ApplyHeadingTable","Squad_GetUpgradesTable","Squad_GetText","Squad_GetCriticalsTable","Squad_ApplyCriticalHitTable","Squad_ModifyDamage","Squad_DropSlotItems","Squad_RemoveUpgrades","Squad_RemoveCritical","Squad_HasCritical","Squad_GetEntityPositionList","Squad_ApplyEntityPositionList","Squad_SetHealthPercentage","Squad_IsVehicle","CCM_ToggleInstantProduction","CCM_ToggleFOW","CCM_ToggleGlobalAI","CCM_ToggleSelectionInvulnerability","CCM_ToggleSelectionOwner","CCM_ToggleDisableWeapons","CCM_ToggleEngineOrPostureState","CCM_ToggleHealthMonitor","CCM_GetSquadKey","CCM_GetEntityKey","CCM_HealthMonitor","_CCM_HealthMonitor_HandleHealth","_CCM_HealthMonitor_KickerMessage","CTF_PreInit","CTFSystem_Init","CTF_GetRandomFlagSpawnPosition","CTF_FreeFlagSpawnPosition","CTFSystem_InitDelayed","CTF_StartCore","CTF_GetWinScoreLimit","CTF_FlagScoreMonitor","CTF_BlinkFlagCarriers","CTF_FlagRespawnMonitor","CTF_FlagStateMonitor","CTF_EnableResources","CTF_UpdateObjectiveUI","CTF_FixFlagColor","CTF_FixFlagColorDelayed","CTF_StopAlarm","CTF_FlagCarrierAbilityExecuted","CTF_DropFlagRequestManager","CTF_PreventFlagCarrierReCrewAndVehicleGarrisoning","CTF_FlagCaptured","CTF_FlagDropped","CTF_FlagScored","CTF_GetOpposingTeam","CTF_TeamFlagScore","CTF_ObjctiveInit","CTF_AddObjectiveUI","Entity_IsReCrewable","Squad_AddFlagCarrierEffects","Squad_RemoveFlagCarrierEffects","Squad_SetCaptureEnabled","Squad_EnableFlagCarrierUI","Squad_EnableCantHoldUI","Squad_ModifyInfantrySpeed","Squad_MonitorDeath","Squad_DisableFlagCarrierUI","isset","Squad_FlagCarrierDeath","Squad_DropFlag","Squad_CarriesFlag","Squad_IsRegisteredFlagCarrier","Squad_RegisterFlagCarrier","Squad_UnRegisterFlagCarrier","Squad_GetSlotItemTable","Player_EnableMoveFlagHereUI","Player_UnlockRetreat","Player_IsHoldingAnyFlags","SGroup_IsCarryingFlag","UI_LocalKickerMessage","UI_GlobalKickerMessage","CTF_Msg","ClearCTF_Msg","Listener","Table_Shuffle","Ability_GetUniqueKey","Player_AddPopulation","Player_ExecuteLocally","Player_GetEnemyPlayer","Player_SetResourceIncomeNumber","Team_GetFirstPlayer","Team_GetEntitiesNearPoint","Team_GetSquadsNearPoint","Team_GetPlayerCount","Team_ExecuteLocally","SGroup_CreateTemp","EGroup_CreateTemp","EGroup_GetClosest","EGroup_AddGroup","EGroup_FilterByUnitType","Entity_GetGarrisonedSquads","Entity_AutoAlign","Entity_CreateAndSpawnToward","Entity_CreateAndSpawnTowardDelayed","Entity_CreateAndSpawnTowardDelayedRandom","Entity_GetName","Entity_GetTempEGroup","Entity_GetOwnerSafe","Entity_Replace","Entity_IsSelected","Entity_HasProductionQueueItem","Entity_IsValidEntity","Squad_GetUniqueKey","Squad_GetName","Squad_IsIdle","Squad_IsConcstructing","Squad_IsHeadingToPosition","Squad_ForEachEntity","UI_FlashSquad","Util_Repeat","Util_Delay","Util_IsPositionInPolygon","Util_GetDirectionalOffset","Util_GetDirectionalOffsetPosition","Util_GetRandomPos","Util_GetAngleTowardsPos","Util_CopyPosition","Util_CreateUIFrame","Util_Tester","Misc_UnSelectAll","Util_DefaultValue","World_ForEachEntitiesByBlueprint","World_GetEntitiesOfType","Pos_AddHeight","Pos_GetString","dr_text3dpos","Heading_Rotate","Squad_InfraRedReveal","WinCondition_PreInit","WinCondition_MonitorVictoryPoints","Team_GetTitle","Team_GetOpposingTeam","OKWNoCache_PreInit","PK_SystemInit","PK_ScanPlayers","PK_PlayerAbilityListener","Player_RemoveTankDispatchAbilities","Squad_GetTempSGroup","Player_GetMapEntryPositionClosest","Util_SortPositionsByClosestImproved","Pos_GetXYZString","PK_Msg","PK_ClearMSG","PK_GetAbilityBleprint","PK_GetUpgradeBleprint","PK_GetSquadBleprint","PK_EventCue","Player_GetRaceIndex","PK_PreInit","RotateThings","TC_PreInit","TC_Init","TC_TogglePlayerCategory","System_PlayerAbilityComplete","System_PlayerAbilityExecuted","TC_UpdatePlayerCircle","TC_UpdatePlayerArrow","TC_GeneralManager","TC_GetAbilityBlueprint","TC_GetMineIcon","TC_GetMineIconScale","TC_MineIsAllowedToMark","TC_MineIsPartOfSMineField","TC_GetMineMarkerColor","TC_GetIcon","TC_MineMarkerManager","TC_BlibMinePlanted","System_EntityConstructionCompleted","System_EntityKilled","Player_IsLocalPlayer","Player_GetUniqueKey","Player_GetName","Players_ForEach","Players_ForEachInTeam","Entity_GetPlayerOwnerSafe","Entity_GetUniqueKey","Entity_CheckForParentSquad","EntityList_ContainsValidEntities","EGroup_GetEntityIds","Util_GlobalMessage","Util_CreateLocString","Util_GetBlueprint","Util_GetUnitOwner","Game_GetLocalPlayerID","World_OwnsUnit","World_GetEntitiesByBlueprint","World_ForEachEntities","Msg","TC_DataInit_Ebps","TC_DataInit","WinCondition_GameOver","WinCondition_Check","WinCondition_Init","$","AAGUID","ANGLE_instanced_arrays","AbstractWorker","AbstractWorkerEventMap","Account","ActiveXObject","AesCbcParams","AesCfbParams","AesCmacParams","AesCtrParams","AesDerivedKeyParams","AesGcmParams","AesKeyAlgorithm","AesKeyGenParams","Algorithm","AlgorithmIdentifier","AnalyserNode","AnimationEvent","AnimationEventInit","ApplicationCache","ApplicationCacheEventMap","Array","ArrayBuffer","ArrayBufferConstructor","ArrayBufferView","ArrayConstructor","ArrayLike","AssertionOptions","AssignedNodesOptions","Attr","Audio","AudioBuffer","AudioBufferSourceNode","AudioBufferSourceNodeEventMap","AudioContext","AudioContextBase","AudioContextEventMap","AudioDestinationNode","AudioListener","AudioNode","AudioParam","AudioProcessingEvent","AudioTrack","AudioTrackList","AudioTrackListEventMap","BarProp","BaseJQueryEventObject","BeforeUnloadEvent","BiquadFilterNode","Blob","BlobPropertyBag","Body","BodyInit","Boolean","BooleanConstructor","Buffer","BufferEncoding","BufferSource","ByteString","CDATASection","CSS","CSSConditionRule","CSSFontFaceRule","CSSGroupingRule","CSSImportRule","CSSKeyframeRule","CSSKeyframesRule","CSSMediaRule","CSSNamespaceRule","CSSPageRule","CSSRule","CSSRuleList","CSSStyleDeclaration","CSSStyleRule","CSSStyleSheet","CSSSupportsRule","Cache","CacheQueryOptions","CacheStorage","Canvas2DContextAttributes","CanvasGradient","CanvasPathMethods","CanvasPattern","CanvasRenderingContext2D","ChannelMergerNode","ChannelSplitterNode","CharacterData","ChildNode","ClassDecorator","ClientData","ClientRect","ClientRectList","ClipboardEvent","ClipboardEventInit","CloseEvent","CloseEventInit","Comment","CompositionEvent","CompositionEventInit","ConcatParams","ConfirmSiteSpecificExceptionsInformation","Console","ConstrainBoolean","ConstrainBooleanParameters","ConstrainDOMString","ConstrainDOMStringParameters","ConstrainDouble","ConstrainDoubleRange","ConstrainLong","ConstrainLongRange","ConstrainVideoFacingModeParameters","ConvolverNode","Coordinates","Crypto","CryptoKey","CryptoKeyPair","CryptoOperationData","CustomElementRegistry","CustomEvent","CustomEventInit","DOMError","DOMException","DOMImplementation","DOML2DeprecatedColorProperty","DOML2DeprecatedSizeProperty","DOMParser","DOMRectInit","DOMSettableTokenList","DOMStringList","DOMStringMap","DOMTokenList","DataCue","DataTransfer","DataTransferItem","DataTransferItemList","DataView","DataViewConstructor","Date","DateConstructor","DecodeErrorCallback","DecodeSuccessCallback","DeferredPermissionRequest","DelayNode","DeviceAcceleration","DeviceAccelerationDict","DeviceLightEvent","DeviceLightEventInit","DeviceMotionEvent","DeviceMotionEventInit","DeviceOrientationEvent","DeviceOrientationEventInit","DeviceRotationRate","DeviceRotationRateDict","DhImportKeyParams","DhKeyAlgorithm","DhKeyDeriveParams","DhKeyGenParams","Document","DocumentEvent","DocumentEventMap","DocumentFragment","DocumentOrShadowRoot","DocumentType","DoubleRange","DragEvent","DynamicsCompressorNode","EXT_frag_depth","EXT_texture_filter_anisotropic","EcKeyAlgorithm","EcKeyGenParams","EcKeyImportParams","EcdhKeyDeriveParams","EcdsaParams","Element","ElementDefinitionOptions","ElementEventMap","ElementListTagNameMap","ElementTagNameMap","ElementTraversal","Enumerator","EnumeratorConstructor","ErrnoException","Error","ErrorConstructor","ErrorEvent","ErrorEventHandler","ErrorEventInit","EvalError","EvalErrorConstructor","Event","EventEmitter","EventInit","EventListener","EventListenerObject","EventListenerOrEventListenerObject","EventModifierInit","EventTarget","ExceptionInformation","ExtensionScriptApis","External","FFF","FGHJK","File","FileList","FilePropertyBag","FileReader","Float32Array","Float32ArrayConstructor","Float64Array","Float64ArrayConstructor","FocusEvent","FocusEventInit","FocusNavigationEvent","FocusNavigationEventInit","FocusNavigationOrigin","Foo","Foos","ForEachCallback","FormData","FrameRequestCallback","Function","FunctionConstructor","FunctionStringCallback","GLbitfield","GLboolean","GLbyte","GLclampf","GLenum","GLfloat","GLint","GLintptr","GLshort","GLsizei","GLsizeiptr","GLubyte","GLuint","GLushort","GainNode","Gamepad","GamepadButton","GamepadEvent","GamepadEventInit","GeneratorFunction","GeneratorFunctionConstructor","Geolocation","GetNotificationOptions","GetSVGDocument","GlobalEventHandlers","GlobalEventHandlersEventMap","GlobalFetch","HTMLAllCollection","HTMLAnchorElement","HTMLAppletElement","HTMLAreaElement","HTMLAreasCollection","HTMLAudioElement","HTMLBRElement","HTMLBaseElement","HTMLBaseFontElement","HTMLBodyElement","HTMLBodyElementEventMap","HTMLButtonElement","HTMLCanvasElement","HTMLCollection","HTMLCollectionBase","HTMLCollectionOf","HTMLDListElement","HTMLDataElement","HTMLDataListElement","HTMLDirectoryElement","HTMLDivElement","HTMLDocument","HTMLElement","HTMLElementEventMap","HTMLElementTagNameMap","HTMLEmbedElement","HTMLFieldSetElement","HTMLFontElement","HTMLFormControlsCollection","HTMLFormElement","HTMLFrameElement","HTMLFrameElementEventMap","HTMLFrameSetElement","HTMLFrameSetElementEventMap","HTMLHRElement","HTMLHeadElement","HTMLHeadingElement","HTMLHtmlElement","HTMLIFrameElement","HTMLIFrameElementEventMap","HTMLImageElement","HTMLInputElement","HTMLLIElement","HTMLLabelElement","HTMLLegendElement","HTMLLinkElement","HTMLMapElement","HTMLMarqueeElement","HTMLMarqueeElementEventMap","HTMLMediaElement","HTMLMediaElementEventMap","HTMLMenuElement","HTMLMetaElement","HTMLMeterElement","HTMLModElement","HTMLOListElement","HTMLObjectElement","HTMLOptGroupElement","HTMLOptionElement","HTMLOptionsCollection","HTMLOutputElement","HTMLParagraphElement","HTMLParamElement","HTMLPictureElement","HTMLPreElement","HTMLProgressElement","HTMLQuoteElement","HTMLScriptElement","HTMLSelectElement","HTMLSlotElement","HTMLSourceElement","HTMLSpanElement","HTMLStyleElement","HTMLTableAlignment","HTMLTableCaptionElement","HTMLTableCellElement","HTMLTableColElement","HTMLTableDataCellElement","HTMLTableElement","HTMLTableHeaderCellElement","HTMLTableRowElement","HTMLTableSectionElement","HTMLTemplateElement","HTMLTextAreaElement","HTMLTimeElement","HTMLTitleElement","HTMLTrackElement","HTMLUListElement","HTMLUnknownElement","HTMLVideoElement","HTMLVideoElementEventMap","HashChangeEvent","HashChangeEventInit","Headers","HeadersInit","History","HkdfCtrParams","HmacImportParams","HmacKeyAlgorithm","HmacKeyGenParams","I","IArguments","IDBArrayKey","IDBCursor","IDBCursorWithValue","IDBDatabase","IDBDatabaseEventMap","IDBEnvironment","IDBFactory","IDBIndex","IDBIndexParameters","IDBKeyPath","IDBKeyRange","IDBObjectStore","IDBObjectStoreParameters","IDBOpenDBRequest","IDBOpenDBRequestEventMap","IDBRequest","IDBRequestEventMap","IDBTransaction","IDBTransactionEventMap","IDBValidKey","IDBVersionChangeEvent","IFoos","IIRFilterNode","ITextWriter","Image","ImageData","Infinity","Int16Array","Int16ArrayConstructor","Int32Array","Int32ArrayConstructor","Int8Array","Int8ArrayConstructor","IntersectionObserver","IntersectionObserverCallback","IntersectionObserverEntry","IntersectionObserverEntryInit","IntersectionObserverInit","Intl","Iterable","IterableIterator","Iterator","IteratorResult","JQuery","JQueryAjaxSettings","JQueryAnimationOptions","JQueryCallback","JQueryCoordinates","JQueryDeferred","JQueryEventConstructor","JQueryEventObject","JQueryGenericPromise","JQueryInputEventObject","JQueryKeyEventObject","JQueryMouseEventObject","JQueryParam","JQueryPromise","JQueryPromiseCallback","JQueryPromiseOperator","JQuerySerializeArrayElement","JQueryStatic","JQuerySupport","JQueryXHR","JSON","JSX","JsonWebKey","KeyAlgorithm","KeyFormat","KeyType","KeyUsage","KeyboardEvent","KeyboardEventInit","LinkStyle","ListeningStateChangedEvent","Location","LongRange","LongRunningScriptDetectedEvent","MSAccountInfo","MSApp","MSAppAsyncOperation","MSAppAsyncOperationEventMap","MSAssertion","MSAudioLocalClientEvent","MSAudioRecvPayload","MSAudioRecvSignal","MSAudioSendPayload","MSAudioSendSignal","MSBaseReader","MSBaseReaderEventMap","MSBlobBuilder","MSConnectivity","MSCredentialFilter","MSCredentialParameters","MSCredentialSpec","MSCredentials","MSDelay","MSDescription","MSExecAtPriorityFunctionCallback","MSFIDOCredentialAssertion","MSFIDOCredentialParameters","MSFIDOSignature","MSFIDOSignatureAssertion","MSFileSaver","MSGesture","MSGestureEvent","MSGraphicsTrust","MSHTMLWebViewElement","MSIPAddressInfo","MSIceWarningFlags","MSInboundPayload","MSInputMethodContext","MSInputMethodContextEventMap","MSJitter","MSLaunchUriCallback","MSLocalClientEvent","MSLocalClientEventBase","MSManipulationEvent","MSMediaKeyError","MSMediaKeyMessageEvent","MSMediaKeyNeededEvent","MSMediaKeySession","MSMediaKeys","MSNavigatorDoNotTrack","MSNetwork","MSNetworkConnectivityInfo","MSNetworkInterfaceType","MSOutboundNetwork","MSOutboundPayload","MSPacketLoss","MSPayloadBase","MSPointerEvent","MSPortRange","MSRangeCollection","MSRelayAddress","MSSignatureParameters","MSSiteModeEvent","MSStream","MSStreamReader","MSTransportDiagnosticsStats","MSUnsafeFunctionCallback","MSUtilization","MSVideoPayload","MSVideoRecvPayload","MSVideoResolutionDistribution","MSVideoSendPayload","MSWebViewAsyncOperation","MSWebViewAsyncOperationEventMap","MSWebViewSettings","Map","MapConstructor","Math","MediaDeviceInfo","MediaDevices","MediaDevicesEventMap","MediaElementAudioSourceNode","MediaEncryptedEvent","MediaEncryptedEventInit","MediaError","MediaKeyMessageEvent","MediaKeyMessageEventInit","MediaKeySession","MediaKeyStatusMap","MediaKeySystemAccess","MediaKeySystemConfiguration","MediaKeySystemMediaCapability","MediaKeys","MediaList","MediaQueryList","MediaQueryListListener","MediaSource","MediaStream","MediaStreamAudioSourceNode","MediaStreamConstraints","MediaStreamError","MediaStreamErrorEvent","MediaStreamErrorEventInit","MediaStreamEvent","MediaStreamEventInit","MediaStreamEventMap","MediaStreamTrack","MediaStreamTrackEvent","MediaStreamTrackEventInit","MediaStreamTrackEventMap","MediaTrackCapabilities","MediaTrackConstraintSet","MediaTrackConstraints","MediaTrackSettings","MediaTrackSupportedConstraints","MessageChannel","MessageEvent","MessageEventInit","MessagePort","MessagePortEventMap","MethodDecorator","MimeType","MimeTypeArray","Model123","Model456","MouseEvent","MouseEventInit","MouseWheelEvent","MsZoomToOptions","MutationCallback","MutationEvent","MutationObserver","MutationObserverInit","MutationRecord","NaN","NamedNodeMap","NavigationCompletedEvent","NavigationEvent","NavigationEventWithReferrer","Navigator","NavigatorBeacon","NavigatorConcurrentHardware","NavigatorContentUtils","NavigatorGeolocation","NavigatorID","NavigatorOnLine","NavigatorStorageUtils","NavigatorUserMedia","NavigatorUserMediaErrorCallback","NavigatorUserMediaSuccessCallback","Node","NodeBuffer","NodeFilter","NodeIterator","NodeJS","NodeList","NodeListOf","NodeModule","NodeProcess","NodeRequire","NodeRequireFunction","NodeSelector","Notification","NotificationEventMap","NotificationOptions","NotificationPermissionCallback","Number","NumberConstructor","OES_element_index_uint","OES_standard_derivatives","OES_texture_float","OES_texture_float_linear","OES_texture_half_float","OES_texture_half_float_linear","Object","ObjectConstructor","ObjectURLOptions","OfflineAudioCompletionEvent","OfflineAudioContext","OfflineAudioContextEventMap","Option","OscillatorNode","OscillatorNodeEventMap","OverflowEvent","PageTransitionEvent","PannerNode","ParameterDecorator","ParentNode","Partial","Path2D","PaymentAddress","PaymentCurrencyAmount","PaymentDetails","PaymentDetailsModifier","PaymentItem","PaymentMethodData","PaymentOptions","PaymentRequest","PaymentRequestEventMap","PaymentRequestUpdateEvent","PaymentRequestUpdateEventInit","PaymentResponse","PaymentShippingOption","Pbkdf2Params","PerfWidgetExternal","Performance","PerformanceEntry","PerformanceMark","PerformanceMeasure","PerformanceNavigation","PerformanceNavigationTiming","PerformanceResourceTiming","PerformanceTiming","PeriodicWave","PeriodicWaveConstraints","PermissionRequest","PermissionRequestedEvent","Pick","Plugin","PluginArray","PointerEvent","PointerEventInit","PopStateEvent","PopStateEventInit","Position","PositionCallback","PositionError","PositionErrorCallback","PositionOptions","ProcessingInstruction","ProgressEvent","ProgressEventInit","Promise","PromiseConstructor","PromiseConstructorLike","PromiseLike","PromiseRejectionEvent","PromiseRejectionEventInit","PropertyDecorator","PropertyDescriptor","PropertyDescriptorMap","PropertyKey","Proxy","ProxyConstructor","ProxyHandler","PushManager","PushSubscription","PushSubscriptionOptions","PushSubscriptionOptionsInit","RTCConfiguration","RTCDTMFToneChangeEvent","RTCDTMFToneChangeEventInit","RTCDtlsFingerprint","RTCDtlsParameters","RTCDtlsTransport","RTCDtlsTransportEventMap","RTCDtlsTransportStateChangedEvent","RTCDtmfSender","RTCDtmfSenderEventMap","RTCIceCandidate","RTCIceCandidateAttributes","RTCIceCandidateComplete","RTCIceCandidateDictionary","RTCIceCandidateInit","RTCIceCandidatePair","RTCIceCandidatePairChangedEvent","RTCIceCandidatePairStats","RTCIceGatherCandidate","RTCIceGatherOptions","RTCIceGatherer","RTCIceGathererEvent","RTCIceGathererEventMap","RTCIceParameters","RTCIceServer","RTCIceTransport","RTCIceTransportEventMap","RTCIceTransportStateChangedEvent","RTCInboundRTPStreamStats","RTCMediaStreamTrackStats","RTCOfferOptions","RTCOutboundRTPStreamStats","RTCPeerConnection","RTCPeerConnectionErrorCallback","RTCPeerConnectionEventMap","RTCPeerConnectionIceEvent","RTCPeerConnectionIceEventInit","RTCRTPStreamStats","RTCRtcpFeedback","RTCRtcpParameters","RTCRtpCapabilities","RTCRtpCodecCapability","RTCRtpCodecParameters","RTCRtpContributingSource","RTCRtpEncodingParameters","RTCRtpFecParameters","RTCRtpHeaderExtension","RTCRtpHeaderExtensionParameters","RTCRtpParameters","RTCRtpReceiver","RTCRtpReceiverEventMap","RTCRtpRtxParameters","RTCRtpSender","RTCRtpSenderEventMap","RTCRtpUnhandled","RTCSessionDescription","RTCSessionDescriptionCallback","RTCSessionDescriptionInit","RTCSrtpKeyParam","RTCSrtpSdesParameters","RTCSrtpSdesTransport","RTCSrtpSdesTransportEventMap","RTCSsrcConflictEvent","RTCSsrcRange","RTCStats","RTCStatsCallback","RTCStatsProvider","RTCStatsReport","RTCTransport","RTCTransportStats","RandomSource","Range","RangeError","RangeErrorConstructor","React","ReadableStream","ReadableStreamReader","Readonly","ReadonlyArray","ReadonlyMap","ReadonlySet","Record","ReferenceError","ReferenceErrorConstructor","Reflect","RegExp","RegExpConstructor","RegExpExecArray","RegExpMatchArray","RegistrationOptions","Request","RequestInfo","RequestInit","Response","ResponseInit","RsaHashedImportParams","RsaHashedKeyAlgorithm","RsaHashedKeyGenParams","RsaKeyAlgorithm","RsaKeyGenParams","RsaOaepParams","RsaOtherPrimesInfo","RsaPssParams","SVGAElement","SVGAngle","SVGAnimatedAngle","SVGAnimatedBoolean","SVGAnimatedEnumeration","SVGAnimatedInteger","SVGAnimatedLength","SVGAnimatedLengthList","SVGAnimatedNumber","SVGAnimatedNumberList","SVGAnimatedPoints","SVGAnimatedPreserveAspectRatio","SVGAnimatedRect","SVGAnimatedString","SVGAnimatedTransformList","SVGCircleElement","SVGClipPathElement","SVGComponentTransferFunctionElement","SVGDefsElement","SVGDescElement","SVGElement","SVGElementEventMap","SVGElementInstance","SVGElementInstanceList","SVGEllipseElement","SVGFEBlendElement","SVGFEColorMatrixElement","SVGFEComponentTransferElement","SVGFECompositeElement","SVGFEConvolveMatrixElement","SVGFEDiffuseLightingElement","SVGFEDisplacementMapElement","SVGFEDistantLightElement","SVGFEFloodElement","SVGFEFuncAElement","SVGFEFuncBElement","SVGFEFuncGElement","SVGFEFuncRElement","SVGFEGaussianBlurElement","SVGFEImageElement","SVGFEMergeElement","SVGFEMergeNodeElement","SVGFEMorphologyElement","SVGFEOffsetElement","SVGFEPointLightElement","SVGFESpecularLightingElement","SVGFESpotLightElement","SVGFETileElement","SVGFETurbulenceElement","SVGFilterElement","SVGFilterPrimitiveStandardAttributes","SVGFitToViewBox","SVGForeignObjectElement","SVGGElement","SVGGradientElement","SVGGraphicsElement","SVGImageElement","SVGLength","SVGLengthList","SVGLineElement","SVGLinearGradientElement","SVGMarkerElement","SVGMaskElement","SVGMatrix","SVGMetadataElement","SVGNumber","SVGNumberList","SVGPathElement","SVGPathSeg","SVGPathSegArcAbs","SVGPathSegArcRel","SVGPathSegClosePath","SVGPathSegCurvetoCubicAbs","SVGPathSegCurvetoCubicRel","SVGPathSegCurvetoCubicSmoothAbs","SVGPathSegCurvetoCubicSmoothRel","SVGPathSegCurvetoQuadraticAbs","SVGPathSegCurvetoQuadraticRel","SVGPathSegCurvetoQuadraticSmoothAbs","SVGPathSegCurvetoQuadraticSmoothRel","SVGPathSegLinetoAbs","SVGPathSegLinetoHorizontalAbs","SVGPathSegLinetoHorizontalRel","SVGPathSegLinetoRel","SVGPathSegLinetoVerticalAbs","SVGPathSegLinetoVerticalRel","SVGPathSegList","SVGPathSegMovetoAbs","SVGPathSegMovetoRel","SVGPatternElement","SVGPoint","SVGPointList","SVGPolygonElement","SVGPolylineElement","SVGPreserveAspectRatio","SVGRadialGradientElement","SVGRect","SVGRectElement","SVGSVGElement","SVGSVGElementEventMap","SVGScriptElement","SVGStopElement","SVGStringList","SVGStyleElement","SVGSwitchElement","SVGSymbolElement","SVGTSpanElement","SVGTests","SVGTextContentElement","SVGTextElement","SVGTextPathElement","SVGTextPositioningElement","SVGTitleElement","SVGTransform","SVGTransformList","SVGURIReference","SVGUnitTypes","SVGUseElement","SVGViewElement","SVGZoomAndPan","SVGZoomEvent","ScopedCredential","ScopedCredentialDescriptor","ScopedCredentialInfo","ScopedCredentialOptions","ScopedCredentialParameters","Screen","ScreenEventMap","ScriptNotifyEvent","ScriptProcessorNode","ScriptProcessorNodeEventMap","ScrollBehavior","ScrollIntoViewOptions","ScrollLogicalPosition","ScrollOptions","ScrollRestoration","ScrollToOptions","Selection","ServiceWorker","ServiceWorkerContainer","ServiceWorkerContainerEventMap","ServiceWorkerEventMap","ServiceWorkerMessageEvent","ServiceWorkerMessageEventInit","ServiceWorkerRegistration","ServiceWorkerRegistrationEventMap","Set","SetConstructor","ShadowRoot","ShadowRootInit","SlowBuffer","SourceBuffer","SourceBufferList","SpeechSynthesis","SpeechSynthesisEvent","SpeechSynthesisEventInit","SpeechSynthesisEventMap","SpeechSynthesisUtterance","SpeechSynthesisUtteranceEventMap","SpeechSynthesisVoice","StereoPannerNode","Storage","StorageEvent","StorageEventInit","StoreExceptionsInformation","StoreSiteSpecificExceptionsInformation","String","StringConstructor","StyleMedia","StyleSheet","StyleSheetList","StyleSheetPageList","SubtleCrypto","Symbol","SymbolConstructor","SyncManager","SyntaxError","SyntaxErrorConstructor","TemplateStringsArray","Text","TextEvent","TextMetrics","TextStreamBase","TextStreamReader","TextStreamWriter","TextTrack","TextTrackCue","TextTrackCueEventMap","TextTrackCueList","TextTrackEventMap","TextTrackList","TextTrackListEventMap","Thenable","TimeRanges","Touch","TouchEvent","TouchList","TrackEvent","TrackEventInit","TransitionEvent","TransitionEventInit","TreeWalker","TypeError","TypeErrorConstructor","TypedPropertyDescriptor","UIEvent","UIEventInit","URIError","URIErrorConstructor","URL","URLSearchParams","USVString","Uint16Array","Uint16ArrayConstructor","Uint32Array","Uint32ArrayConstructor","Uint8Array","Uint8ArrayConstructor","Uint8ClampedArray","Uint8ClampedArrayConstructor","UnviewableContentIdentifiedEvent","VBArray","VBArrayConstructor","ValidityState","VarDate","VideoPlaybackQuality","VideoTrack","VideoTrackList","VideoTrackListEventMap","VoidFunction","WEBGL_compressed_texture_s3tc","WEBGL_debug_renderer_info","WEBGL_depth_texture","WScript","WaveShaperNode","WeakMap","WeakMapConstructor","WeakSet","WeakSetConstructor","WebAuthentication","WebAuthnAssertion","WebAuthnExtensions","WebGLActiveInfo","WebGLBuffer","WebGLContextAttributes","WebGLContextEvent","WebGLContextEventInit","WebGLFramebuffer","WebGLObject","WebGLProgram","WebGLRenderbuffer","WebGLRenderingContext","WebGLShader","WebGLShaderPrecisionFormat","WebGLTexture","WebGLUniformLocation","WebKitCSSMatrix","WebKitDirectoryEntry","WebKitDirectoryReader","WebKitEntriesCallback","WebKitEntry","WebKitErrorCallback","WebKitFileCallback","WebKitFileEntry","WebKitFileSystem","WebKitPoint","WebSocket","WebSocketEventMap","WheelEvent","WheelEventInit","Window","WindowBase64","WindowConsole","WindowEventMap","WindowLocalStorage","WindowSessionStorage","WindowTimers","WindowTimersExtension","Worker","WorkerEventMap","WritableStream","XMLDocument","XMLHttpRequest","XMLHttpRequestEventMap","XMLHttpRequestEventTarget","XMLHttpRequestEventTargetEventMap","XMLHttpRequestUpload","XMLSerializer","XPathEvaluator","XPathExpression","XPathNSResolver","XPathResult","XSLTProcessor","_","__dirname","__filename","a","abstract","addEventListener","alert","any","applicationCache","as","async","atob","await","b","blur","boolean","break","btoa","caches","cancelAnimationFrame","captureEvents","case","catch","class","clearImmediate","clearInterval","clearTimeout","clientInformation","close","closed","confirm","console","const","constructor","continue","count","crypto","customElements","dddd","debugger","declare","decodeURI","decodeURIComponent","default","defaultStatus","delete","departFocus","devicePixelRatio","dispatchEvent","do","doIt","doNotTrack","doUpdateSnippet","document","element","else","encodeURI","encodeURIComponent","enum","eval","event","export","exports","extends","external","false","fetch","finally","findSnippetById","focus","foo","foon","fooo","for","frameElement","frames","from","function","fuzzy_match","fuzzy_match_simple","get","getComputedStyle","getMatchedCSSRules","getSelection","global","global","history","if","implements","import","importScripts","in","indexedDB","innerHeight","innerWidth","instanceof","interface","is","isFinite","isNaN","isSecureContext","jQuery","keyof","length","let","localStorage","location","locationbar","matchMedia","menubar","module","module","more","moveBy","moveTo","msContentScript","msCredentials","msWriteProfilerMark","name","namespace","navigator","never","new","null","number","object","of","offscreenBuffering","onabort","onafterprint","onbeforeprint","onbeforeunload","onblur","oncanplay","oncanplaythrough","onchange","onclick","oncompassneedscalibration","oncontextmenu","ondblclick","ondevicelight","ondevicemotion","ondeviceorientation","ondrag","ondragend","ondragenter","ondragleave","ondragover","ondragstart","ondrop","ondurationchange","onemptied","onended","onerror","onfocus","onhashchange","oninput","oninvalid","onkeydown","onkeypress","onkeyup","onload","onloadeddata","onloadedmetadata","onloadstart","onmessage","onmousedown","onmouseenter","onmouseleave","onmousemove","onmouseout","onmouseover","onmouseup","onmousewheel","onmsgesturechange","onmsgesturedoubletap","onmsgestureend","onmsgesturehold","onmsgesturestart","onmsgesturetap","onmsinertiastart","onmspointercancel","onmspointerdown","onmspointerenter","onmspointerleave","onmspointermove","onmspointerout","onmspointerover","onmspointerup","onoffline","ononline","onorientationchange","onpagehide","onpageshow","onpause","onplay","onplaying","onpointercancel","onpointerdown","onpointerenter","onpointerleave","onpointermove","onpointerout","onpointerover","onpointerup","onpopstate","onprogress","onratechange","onreadystatechange","onreset","onresize","onscroll","onseeked","onseeking","onselect","onstalled","onstorage","onsubmit","onsuspend","ontimeupdate","ontouchcancel","ontouchend","ontouchmove","ontouchstart","onunload","onvolumechange","onwaiting","onwheel","open","opener","orientation","outerHeight","outerWidth","package","pageXOffset","pageYOffset","parent","parseFloat","parseInt","payloadtype","performance","personalbar","postMessage","print","private","process","prompt","protected","public","readonly","releaseEvents","removeEventListener","requestAnimationFrame","require","require","resizeBy","resizeTo","return","screen","screenLeft","screenTop","screenX","screenY","scroll","scrollBy","scrollTo","scrollX","scrollY","scrollbars","self","sessionStorage","set","setImmediate","setInterval","setTimeout","speechSynthesis","static","status","statusbar","stop","string","styleMedia","super","switch","symbol","this","throw","toString","toolbar","top","true","try","type","typedoc","typeof","undefined","undefined","updateSnippet","uuid","vSomething","var","void","webkitCancelAnimationFrame","webkitConvertPointFromNodeToPage","webkitConvertPointFromPageToNode","webkitRTCPeerConnection","webkitRequestAnimationFrame","while","window","with","yield"] -}; }); \ No newline at end of file +}; }); diff --git a/src/vs/base/test/common/glob.test.ts b/src/vs/base/test/common/glob.test.ts index beb386d3d3..a3e55d5f6b 100644 --- a/src/vs/base/test/common/glob.test.ts +++ b/src/vs/base/test/common/glob.test.ts @@ -7,6 +7,7 @@ import * as assert from 'assert'; import * as glob from 'vs/base/common/glob'; import { sep } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; +import { URI } from 'vs/base/common/uri'; suite('Glob', () => { @@ -1019,4 +1020,9 @@ suite('Glob', () => { assertNoGlobMatch(p, '/DNXConsoleApp/foo/Program.cs'); } }); + + test('URI match', () => { + let p = 'scheme:/**/*.md'; + assertGlobMatch(p, URI.file('super/duper/long/some/file.md').with({ scheme: 'scheme' }).toString()); + }); }); diff --git a/src/vs/base/test/common/map.test.ts b/src/vs/base/test/common/map.test.ts index 79289dc044..f3fb005f7e 100644 --- a/src/vs/base/test/common/map.test.ts +++ b/src/vs/base/test/common/map.test.ts @@ -762,6 +762,9 @@ suite('Map', () => { iter = map.findSuperstr(URI.file('/'))!; item = iter.next(); + assert.strictEqual(item.value[1], 4); + assert.strictEqual(item.done, false); + item = iter.next(); assert.strictEqual(item.value[1], 2); assert.strictEqual(item.done, false); item = iter.next(); @@ -771,9 +774,6 @@ suite('Map', () => { assert.strictEqual(item.value[1], 3); assert.strictEqual(item.done, false); item = iter.next(); - assert.strictEqual(item.value[1], 4); - assert.strictEqual(item.done, false); - item = iter.next(); assert.strictEqual(item.value, undefined); assert.strictEqual(item.done, true); diff --git a/src/vs/base/test/common/mime.test.ts b/src/vs/base/test/common/mime.test.ts index cbf4f0ff8d..4a11cdd407 100644 --- a/src/vs/base/test/common/mime.test.ts +++ b/src/vs/base/test/common/mime.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { guessMimeTypes, registerTextMime } from 'vs/base/common/mime'; +import { guessMimeTypes, normalizeMimeType, registerTextMime } from 'vs/base/common/mime'; import { URI } from 'vs/base/common/uri'; suite('Mime', () => { @@ -126,4 +126,13 @@ suite('Mime', () => { assert.deepStrictEqual(guessMimeTypes(URI.parse(`data:;label:something.data;description:data,`)), ['text/data', 'text/plain']); }); + + test('normalize', () => { + assert.strictEqual(normalizeMimeType('invalid'), 'invalid'); + assert.strictEqual(normalizeMimeType('invalid', true), undefined); + assert.strictEqual(normalizeMimeType('Text/plain'), 'text/plain'); + assert.strictEqual(normalizeMimeType('Text/pläin'), 'text/pläin'); + assert.strictEqual(normalizeMimeType('Text/plain;UPPER'), 'text/plain;UPPER'); + assert.strictEqual(normalizeMimeType('Text/plain;lower'), 'text/plain;lower'); + }); }); diff --git a/src/vs/base/test/common/processes.test.ts b/src/vs/base/test/common/processes.test.ts index c20184692b..54a5dd02fe 100644 --- a/src/vs/base/test/common/processes.test.ts +++ b/src/vs/base/test/common/processes.test.ts @@ -21,7 +21,7 @@ suite('Processes', () => { VSCODE_NLS_CONFIG: 'x', VSCODE_PORTABLE: 'x', VSCODE_PID: 'x', - VSCODE_NODE_CACHED_DATA_DIR: 'x', + VSCODE_CODE_CACHE_PATH: 'x', VSCODE_NEW_VAR: 'x', GDK_PIXBUF_MODULE_FILE: 'x', GDK_PIXBUF_MODULEDIR: 'x', diff --git a/src/vs/base/test/common/stream.test.ts b/src/vs/base/test/common/stream.test.ts index a28b604af3..03b22306b2 100644 --- a/src/vs/base/test/common/stream.test.ts +++ b/src/vs/base/test/common/stream.test.ts @@ -91,7 +91,7 @@ suite('Stream', () => { }); test('WriteableStream - end with error works', async () => { - const reducer = (errors: Error[]) => errors.length > 0 ? errors[0] : null as unknown as Error; + const reducer = (errors: Error[]) => errors[0]; const stream = newWriteableStream(reducer); stream.end(new Error('error')); diff --git a/src/vs/base/test/common/testUtils.ts b/src/vs/base/test/common/testUtils.ts new file mode 100644 index 0000000000..267de568c1 --- /dev/null +++ b/src/vs/base/test/common/testUtils.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export function flakySuite(title: string, fn: () => void) /* Suite */ { + return suite(title, function () { + + // Flaky suites need retries and timeout to complete + // e.g. because they access browser features 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/crypto.test.ts b/src/vs/base/test/node/crypto.test.ts index 035dc5a4ad..d6a24e8308 100644 --- a/src/vs/base/test/node/crypto.test.ts +++ b/src/vs/base/test/node/crypto.test.ts @@ -6,8 +6,7 @@ import { checksum } from 'vs/base/node/crypto'; import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; -import { promises } from 'fs'; -import { rimraf, writeFile } from 'vs/base/node/pfs'; +import { Promises } from 'vs/base/node/pfs'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; flakySuite('Crypto', () => { @@ -17,16 +16,16 @@ flakySuite('Crypto', () => { setup(function () { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'crypto'); - return promises.mkdir(testDir, { recursive: true }); + return Promises.mkdir(testDir, { recursive: true }); }); teardown(function () { - return rimraf(testDir); + return Promises.rm(testDir); }); test('checksum', async () => { const testFile = join(testDir, 'checksum.txt'); - await writeFile(testFile, 'Hello World'); + await Promises.writeFile(testFile, 'Hello World'); await checksum(testFile, '0a4d55a8d778e5022fab701977c5d840bbc486d0'); }); diff --git a/src/vs/base/test/node/extpath.test.ts b/src/vs/base/test/node/extpath.test.ts index 5042565b7c..8f0694fdfb 100644 --- a/src/vs/base/test/node/extpath.test.ts +++ b/src/vs/base/test/node/extpath.test.ts @@ -5,8 +5,7 @@ import * as assert from 'assert'; import { tmpdir } from 'os'; -import { promises } from 'fs'; -import { rimraf } from 'vs/base/node/pfs'; +import { Promises } from 'vs/base/node/pfs'; import { realcaseSync, realpath, realpathSync } from 'vs/base/node/extpath'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; @@ -16,11 +15,11 @@ flakySuite('Extpath', () => { setup(() => { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'extpath'); - return promises.mkdir(testDir, { recursive: true }); + return Promises.mkdir(testDir, { recursive: true }); }); teardown(() => { - return rimraf(testDir); + return Promises.rm(testDir); }); test('realcase', async () => { diff --git a/src/vs/base/test/node/pfs/fixtures/site.css b/src/vs/base/test/node/pfs/fixtures/site.css index b7e5283202..c5cea74684 100644 --- a/src/vs/base/test/node/pfs/fixtures/site.css +++ b/src/vs/base/test/node/pfs/fixtures/site.css @@ -25,12 +25,12 @@ h1, h2, h3, h4, h5, h6 margin: 0px; } -textarea +textarea { font-family: Consolas } -#results +#results { margin-top: 2em; margin-left: 2em; diff --git a/src/vs/base/test/node/pfs/pfs.test.ts b/src/vs/base/test/node/pfs/pfs.test.ts index abc3f45df6..a664be42ca 100644 --- a/src/vs/base/test/node/pfs/pfs.test.ts +++ b/src/vs/base/test/node/pfs/pfs.test.ts @@ -8,7 +8,7 @@ import * as fs from 'fs'; 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 { Promises, RimRafMode, rimrafSync, SymlinkSupport, writeFileSync } from 'vs/base/node/pfs'; import { timeout } from 'vs/base/common/async'; import { canNormalize } from 'vs/base/common/normalization'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -22,21 +22,21 @@ flakySuite('PFS', function () { setup(() => { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'pfs'); - return fs.promises.mkdir(testDir, { recursive: true }); + return Promises.mkdir(testDir, { recursive: true }); }); teardown(() => { - return rimraf(testDir); + return Promises.rm(testDir); }); test('writeFile', async () => { const testFile = join(testDir, 'writefile.txt'); - assert.ok(!(await exists(testFile))); + assert.ok(!(await Promises.exists(testFile))); - await writeFile(testFile, 'Hello World', (null!)); + await Promises.writeFile(testFile, 'Hello World', (null!)); - assert.strictEqual((await fs.promises.readFile(testFile)).toString(), 'Hello World'); + assert.strictEqual((await Promises.readFile(testFile)).toString(), 'Hello World'); }); test('writeFile - parallel write on different files works', async () => { @@ -47,11 +47,11 @@ flakySuite('PFS', function () { const testFile5 = join(testDir, 'writefile5.txt'); await Promise.all([ - 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!)) + Promises.writeFile(testFile1, 'Hello World 1', (null!)), + Promises.writeFile(testFile2, 'Hello World 2', (null!)), + Promises.writeFile(testFile3, 'Hello World 3', (null!)), + Promises.writeFile(testFile4, 'Hello World 4', (null!)), + Promises.writeFile(testFile5, 'Hello World 5', (null!)) ]); assert.strictEqual(fs.readFileSync(testFile1).toString(), 'Hello World 1'); assert.strictEqual(fs.readFileSync(testFile2).toString(), 'Hello World 2'); @@ -64,11 +64,11 @@ flakySuite('PFS', function () { const testFile = join(testDir, 'writefile.txt'); await Promise.all([ - 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)) + Promises.writeFile(testFile, 'Hello World 1', undefined), + Promises.writeFile(testFile, 'Hello World 2', undefined), + timeout(10).then(() => Promises.writeFile(testFile, 'Hello World 3', undefined)), + Promises.writeFile(testFile, 'Hello World 4', undefined), + timeout(10).then(() => Promises.writeFile(testFile, 'Hello World 5', undefined)) ]); assert.strictEqual(fs.readFileSync(testFile).toString(), 'Hello World 5'); }); @@ -77,7 +77,7 @@ flakySuite('PFS', function () { fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await rimraf(testDir); + await Promises.rm(testDir); assert.ok(!fs.existsSync(testDir)); }); @@ -85,7 +85,7 @@ flakySuite('PFS', function () { fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await rimraf(testDir, RimRafMode.MOVE); + await Promises.rm(testDir, RimRafMode.MOVE); assert.ok(!fs.existsSync(testDir)); }); @@ -95,7 +95,7 @@ flakySuite('PFS', function () { fs.mkdirSync(join(testDir, 'somefolder')); fs.writeFileSync(join(testDir, 'somefolder', 'somefile.txt'), 'Contents'); - await rimraf(testDir); + await Promises.rm(testDir); assert.ok(!fs.existsSync(testDir)); }); @@ -105,7 +105,7 @@ flakySuite('PFS', function () { fs.mkdirSync(join(testDir, 'somefolder')); fs.writeFileSync(join(testDir, 'somefolder', 'somefile.txt'), 'Contents'); - await rimraf(testDir, RimRafMode.MOVE); + await Promises.rm(testDir, RimRafMode.MOVE); assert.ok(!fs.existsSync(testDir)); }); @@ -113,7 +113,7 @@ flakySuite('PFS', function () { fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await rimraf(testDir, RimRafMode.MOVE); + await Promises.rm(testDir, RimRafMode.MOVE); assert.ok(!fs.existsSync(testDir)); }); @@ -121,7 +121,7 @@ flakySuite('PFS', function () { fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await rimraf(`${testDir}${sep}`, RimRafMode.MOVE); + await Promises.rm(`${testDir}${sep}`, RimRafMode.MOVE); assert.ok(!fs.existsSync(testDir)); }); @@ -161,7 +161,7 @@ flakySuite('PFS', function () { const targetDir = join(parentDir, id); const targetDir2 = join(parentDir, id2); - await copy(sourceDir, targetDir, { preserveSymlinks: true }); + await Promises.copy(sourceDir, targetDir, { preserveSymlinks: true }); assert.ok(fs.existsSync(targetDir)); assert.ok(fs.existsSync(join(targetDir, 'index.html'))); @@ -170,7 +170,7 @@ flakySuite('PFS', function () { assert.ok(fs.statSync(join(targetDir, 'examples')).isDirectory()); assert.ok(fs.existsSync(join(targetDir, 'examples', 'small.jxs'))); - await move(targetDir, targetDir2); + await Promises.move(targetDir, targetDir2); assert.ok(!fs.existsSync(targetDir)); assert.ok(fs.existsSync(targetDir2)); @@ -180,12 +180,12 @@ flakySuite('PFS', function () { assert.ok(fs.statSync(join(targetDir2, 'examples')).isDirectory()); assert.ok(fs.existsSync(join(targetDir2, 'examples', 'small.jxs'))); - await move(join(targetDir2, 'index.html'), join(targetDir2, 'index_moved.html')); + await Promises.move(join(targetDir2, 'index.html'), join(targetDir2, 'index_moved.html')); assert.ok(!fs.existsSync(join(targetDir2, 'index.html'))); assert.ok(fs.existsSync(join(targetDir2, 'index_moved.html'))); - await rimraf(parentDir); + await Promises.rm(parentDir); assert.ok(!fs.existsSync(parentDir)); }); @@ -200,7 +200,7 @@ flakySuite('PFS', function () { const id3 = generateUuid(); const copyTarget = join(testDir, id3); - await fs.promises.mkdir(symbolicLinkTarget, { recursive: true }); + await Promises.mkdir(symbolicLinkTarget, { recursive: true }); fs.symlinkSync(symbolicLinkTarget, symLink, 'junction'); @@ -209,7 +209,7 @@ flakySuite('PFS', function () { // Windows: this test does not work because creating symlinks // requires priviledged permissions (admin). if (!isWindows) { - await copy(symLink, copyTarget, { preserveSymlinks: true }); + await Promises.copy(symLink, copyTarget, { preserveSymlinks: true }); assert.ok(fs.existsSync(copyTarget)); @@ -217,13 +217,13 @@ flakySuite('PFS', function () { assert.ok(symbolicLink); assert.ok(!symbolicLink.dangling); - const target = await fs.promises.readlink(copyTarget); + const target = await 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 }); + await Promises.rm(copyTarget); + await Promises.copy(symLink, copyTarget, { preserveSymlinks: false }); assert.ok(fs.existsSync(copyTarget)); @@ -233,10 +233,10 @@ flakySuite('PFS', function () { // Copy does not fail over dangling symlinks - await rimraf(copyTarget); - await rimraf(symbolicLinkTarget); + await Promises.rm(copyTarget); + await Promises.rm(symbolicLinkTarget); - await copy(symLink, copyTarget, { preserveSymlinks: true }); // this should not throw + await Promises.copy(symLink, copyTarget, { preserveSymlinks: true }); // this should not throw if (!isWindows) { const { symbolicLink } = await SymlinkSupport.stat(copyTarget); @@ -253,8 +253,8 @@ flakySuite('PFS', function () { 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'); + await Promises.mkdir(sourceLinkMD5JSFolder, { recursive: true }); + await Promises.writeFile(sourceLinkMD5JSFile, 'Hello from MD5'); const sourceLinkMD5JSFolderLinked = join(sourceLinkTestFolder, 'md5-linked'); // copy-test/link-test/md5-linked fs.symlinkSync(sourceLinkMD5JSFolder, sourceLinkMD5JSFolderLinked, 'junction'); @@ -270,7 +270,7 @@ flakySuite('PFS', function () { // Windows: this test does not work because creating symlinks // requires priviledged permissions (admin). if (!isWindows) { - await copy(sourceLinkTestFolder, targetLinkTestFolder, { preserveSymlinks: true }); + await Promises.copy(sourceLinkTestFolder, targetLinkTestFolder, { preserveSymlinks: true }); assert.ok(fs.existsSync(targetLinkTestFolder)); assert.ok(fs.existsSync(targetLinkMD5JSFolder)); @@ -278,14 +278,14 @@ flakySuite('PFS', function () { assert.ok(fs.existsSync(targetLinkMD5JSFolderLinked)); assert.ok(fs.lstatSync(targetLinkMD5JSFolderLinked).isSymbolicLink()); - const linkTarget = await fs.promises.readlink(targetLinkMD5JSFolderLinked); + const linkTarget = await Promises.readlink(targetLinkMD5JSFolderLinked); assert.strictEqual(linkTarget, targetLinkMD5JSFolder); - await fs.promises.rmdir(targetLinkTestFolder, { recursive: true }); + await Promises.rmdir(targetLinkTestFolder, { recursive: true }); } // Copy with `preserveSymlinks: false` and verify result - await copy(sourceLinkTestFolder, targetLinkTestFolder, { preserveSymlinks: false }); + await Promises.copy(sourceLinkTestFolder, targetLinkTestFolder, { preserveSymlinks: false }); assert.ok(fs.existsSync(targetLinkTestFolder)); assert.ok(fs.existsSync(targetLinkMD5JSFolder)); @@ -301,7 +301,7 @@ flakySuite('PFS', function () { fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - const result = await readDirsInDir(testDir); + const result = await Promises.readDirsInDir(testDir); assert.strictEqual(result.length, 3); assert.ok(result.indexOf('somefolder1') !== -1); assert.ok(result.indexOf('somefolder2') !== -1); @@ -315,7 +315,7 @@ flakySuite('PFS', function () { const id2 = generateUuid(); const symbolicLink = join(testDir, id2); - await fs.promises.mkdir(directory, { recursive: true }); + await Promises.mkdir(directory, { recursive: true }); fs.symlinkSync(directory, symbolicLink, 'junction'); @@ -334,11 +334,11 @@ flakySuite('PFS', function () { const id2 = generateUuid(); const symbolicLink = join(testDir, id2); - await fs.promises.mkdir(directory, { recursive: true }); + await Promises.mkdir(directory, { recursive: true }); fs.symlinkSync(directory, symbolicLink, 'junction'); - await rimraf(directory); + await Promises.rm(directory); const statAndIsLink = await SymlinkSupport.stat(symbolicLink); assert.ok(statAndIsLink?.symbolicLink); @@ -350,11 +350,11 @@ flakySuite('PFS', function () { const id = generateUuid(); const newDir = join(testDir, 'pfs', id, 'öäü'); - await fs.promises.mkdir(newDir, { recursive: true }); + await Promises.mkdir(newDir, { recursive: true }); assert.ok(fs.existsSync(newDir)); - const children = await readdir(join(testDir, 'pfs', id)); + const children = await Promises.readdir(join(testDir, 'pfs', id)); assert.strictEqual(children.some(n => n === 'öäü'), true); // Mac always converts to NFD, so } }); @@ -362,13 +362,13 @@ flakySuite('PFS', function () { test('readdir (with file types)', async () => { if (canNormalize && typeof process.versions['electron'] !== 'undefined' /* needs electron */) { const newDir = join(testDir, 'öäü'); - await fs.promises.mkdir(newDir, { recursive: true }); + await Promises.mkdir(newDir, { recursive: true }); - await writeFile(join(testDir, 'somefile.txt'), 'contents'); + await Promises.writeFile(join(testDir, 'somefile.txt'), 'contents'); assert.ok(fs.existsSync(newDir)); - const children = await readdir(testDir, { withFileTypes: true }); + const children = await Promises.readdir(testDir, { withFileTypes: true }); assert.strictEqual(children.some(n => n.name === 'öäü'), true); // Mac always converts to NFD, so assert.strictEqual(children.some(n => n.isDirectory()), true); @@ -409,10 +409,10 @@ flakySuite('PFS', function () { assert.ok(fs.existsSync(testDir)); - await writeFile(testFile, smallData); + await Promises.writeFile(testFile, smallData); assert.strictEqual(fs.readFileSync(testFile).toString(), smallDataValue); - await writeFile(testFile, bigData); + await Promises.writeFile(testFile, bigData); assert.strictEqual(fs.readFileSync(testFile).toString(), bigDataValue); } @@ -423,7 +423,7 @@ flakySuite('PFS', function () { let expectedError: Error | undefined; try { - await writeFile(testFile, 'Hello World'); + await Promises.writeFile(testFile, 'Hello World'); } catch (error) { expectedError = error; } diff --git a/src/vs/base/test/node/processes/fixtures/fork.ts b/src/vs/base/test/node/processes/fixtures/fork.ts index 1444cbd141..fa885bdd06 100644 --- a/src/vs/base/test/node/processes/fixtures/fork.ts +++ b/src/vs/base/test/node/processes/fixtures/fork.ts @@ -11,4 +11,4 @@ process.on('message', msg => { sender.send(msg); }); -sender.send('ready'); \ No newline at end of file +sender.send('ready'); diff --git a/src/vs/base/test/node/processes/fixtures/fork_large.ts b/src/vs/base/test/node/processes/fixtures/fork_large.ts index f4878b53e0..62e79aa303 100644 --- a/src/vs/base/test/node/processes/fixtures/fork_large.ts +++ b/src/vs/base/test/node/processes/fixtures/fork_large.ts @@ -14,4 +14,4 @@ process.on('message', msg => { sender.send('done'); }); -sender.send('ready'); \ No newline at end of file +sender.send('ready'); diff --git a/src/vs/base/test/node/testUtils.ts b/src/vs/base/test/node/testUtils.ts index 678e3166c8..91eaed423e 100644 --- a/src/vs/base/test/node/testUtils.ts +++ b/src/vs/base/test/node/testUtils.ts @@ -3,10 +3,10 @@ * 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 { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; +import * as testUtils from 'vs/base/test/common/testUtils'; export function getRandomTestPath(tmpdir: string, ...segments: string[]): string { return join(tmpdir, ...segments, generateUuid()); @@ -16,17 +16,4 @@ export function getPathFromAmdModule(requirefn: typeof require, relativePath: st return URI.parse(requirefn.toUrl(relativePath)).fsPath; } -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); - }); -} +export import flakySuite = testUtils.flakySuite; diff --git a/src/vs/base/test/node/zip/zip.test.ts b/src/vs/base/test/node/zip/zip.test.ts index 0d0650c119..817af80a7e 100644 --- a/src/vs/base/test/node/zip/zip.test.ts +++ b/src/vs/base/test/node/zip/zip.test.ts @@ -6,9 +6,8 @@ import * as assert from 'assert'; import * as path from 'vs/base/common/path'; import { tmpdir } from 'os'; -import { promises } from 'fs'; import { extract } from 'vs/base/node/zip'; -import { rimraf, exists } from 'vs/base/node/pfs'; +import { Promises } from 'vs/base/node/pfs'; import { createCancelablePromise } from 'vs/base/common/async'; import { getRandomTestPath, getPathFromAmdModule } from 'vs/base/test/node/testUtils'; @@ -19,11 +18,11 @@ suite('Zip', () => { setup(() => { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'zip'); - return promises.mkdir(testDir, { recursive: true }); + return Promises.mkdir(testDir, { recursive: true }); }); teardown(() => { - return rimraf(testDir); + return Promises.rm(testDir); }); test('extract should handle directories', async () => { @@ -31,7 +30,7 @@ suite('Zip', () => { const fixture = path.join(fixtures, 'extract.zip'); await createCancelablePromise(token => extract(fixture, testDir, {}, token)); - const doesExist = await exists(path.join(testDir, 'extension')); + const doesExist = await Promises.exists(path.join(testDir, 'extension')); assert(doesExist); }); }); diff --git a/src/vs/base/test/parts/quickinput/browser/quickinput.test.ts b/src/vs/base/test/parts/quickinput/browser/quickinput.test.ts new file mode 100644 index 0000000000..55308d1b7c --- /dev/null +++ b/src/vs/base/test/parts/quickinput/browser/quickinput.test.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { List } from 'vs/base/browser/ui/list/listWidget'; +import { QuickInputController } from 'vs/base/parts/quickinput/browser/quickInput'; +import { IWorkbenchListOptions } from 'vs/platform/list/browser/listService'; + +// Simple promisify of setTimeout +function wait(delayMS: number) { + return new Promise(function (resolve) { + setTimeout(resolve, delayMS); + }); +} + +suite('QuickInput', () => { + let fixture: HTMLElement, controller: QuickInputController; + + setup(() => { + fixture = document.createElement('div'); + document.body.appendChild(fixture); + + controller = new QuickInputController({ + container: fixture, + idPrefix: 'testQuickInput', + ignoreFocusOut() { return false; }, + isScreenReaderOptimized() { return false; }, + returnFocus() { }, + backKeybindingLabel() { return undefined; }, + setContextKey() { return undefined; }, + createList: ( + user: string, + container: HTMLElement, + delegate: IListVirtualDelegate, + renderers: IListRenderer[], + options: IWorkbenchListOptions, + ) => new List(user, container, delegate, renderers, options), + styles: { + button: {}, + countBadge: {}, + inputBox: {}, + keybindingLabel: {}, + list: {}, + progressBar: {}, + widget: {} + } + }); + }); + + teardown(() => { + controller.dispose(); + document.body.removeChild(fixture); + }); + + test('onDidChangeValue gets triggered when .value is set', async () => { + const quickpick = controller.createQuickPick(); + + let value: string | undefined = undefined; + quickpick.onDidChangeValue((e) => value = e); + + // Trigger a change + quickpick.value = 'changed'; + + try { + // wait a bit to let the event play out. + await wait(200); + assert.strictEqual(value, quickpick.value); + } finally { + quickpick.dispose(); + } + }); +}); diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index 567183fda9..9c4579f404 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -278,8 +278,8 @@ class WorkspaceProvider implements IWorkspaceProvider { readonly trusted = true; constructor( - public readonly workspace: IWorkspace, - public readonly payload: object + readonly workspace: IWorkspace, + readonly payload: object ) { } async open(workspace: IWorkspace, options?: { reuse?: boolean, payload?: object }): Promise { diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/codeCacheCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/codeCacheCleaner.ts new file mode 100644 index 0000000000..49488406e4 --- /dev/null +++ b/src/vs/code/electron-browser/sharedProcess/contrib/codeCacheCleaner.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { basename, dirname, join } from 'vs/base/common/path'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Promises } from 'vs/base/node/pfs'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { ILogService } from 'vs/platform/log/common/log'; + +export class CodeCacheCleaner extends Disposable { + + private readonly _DataMaxAge = this.productService.quality !== 'stable' + ? 1000 * 60 * 60 * 24 * 7 // roughly 1 week (insiders) + : 1000 * 60 * 60 * 24 * 30 * 3; // roughly 3 months (stable) + + constructor( + currentCodeCachePath: string | undefined, + @IProductService private readonly productService: IProductService, + @ILogService private readonly logService: ILogService + ) { + super(); + + // Cached data is stored as user data and we run a cleanup task everytime + // the editor starts. The strategy is to delete all files that are older than + // 3 months (1 week respectively) + if (currentCodeCachePath) { + const scheduler = this._register(new RunOnceScheduler(() => { + this.cleanUpCodeCaches(currentCodeCachePath); + }, 30 * 1000 /* after 30s */)); + scheduler.schedule(); + } + } + + private async cleanUpCodeCaches(currentCodeCachePath: string): Promise { + this.logService.info('[code cache cleanup]: Starting to clean up old code cache folders.'); + + try { + const now = Date.now(); + + // The folder which contains folders of cached data. + // Each of these folders is partioned per commit + const codeCacheRootPath = dirname(currentCodeCachePath); + const currentCodeCache = basename(currentCodeCachePath); + + const codeCaches = await Promises.readdir(codeCacheRootPath); + await Promise.all(codeCaches.map(async codeCache => { + if (codeCache === currentCodeCache) { + return; // not the current cache folder + } + + // Delete cache folder if old enough + const codeCacheEntryPath = join(codeCacheRootPath, codeCache); + const codeCacheEntryStat = await Promises.stat(codeCacheEntryPath); + if (codeCacheEntryStat.isDirectory() && (now - codeCacheEntryStat.mtime.getTime()) > this._DataMaxAge) { + this.logService.info(`[code cache cleanup]: Removing code cache folder ${codeCache}.`); + + return Promises.rm(codeCacheEntryPath); + } + })); + } catch (error) { + onUnexpectedError(error); + } + } +} diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts index 64e164a39a..25b192ac48 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts @@ -3,17 +3,17 @@ * 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 { join } from 'vs/base/common/path'; +import { Promises } from 'vs/base/node/pfs'; import { IStringDictionary } from 'vs/base/common/collections'; import { IProductService } from 'vs/platform/product/common/productService'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { onUnexpectedError } from 'vs/base/common/errors'; import { ILogService } from 'vs/platform/log/common/log'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { RunOnceScheduler } from 'vs/base/common/async'; -interface ExtensionEntry { +interface IExtensionEntry { version: string; extensionIdentifier: { id: string; @@ -21,87 +21,88 @@ interface ExtensionEntry { }; } -interface LanguagePackEntry { +interface ILanguagePackEntry { hash: string; - extensions: ExtensionEntry[]; + extensions: IExtensionEntry[]; } -interface LanguagePackFile { - [locale: string]: LanguagePackEntry; +interface ILanguagePackFile { + [locale: string]: ILanguagePackEntry; } export class LanguagePackCachedDataCleaner extends Disposable { - private readonly _DataMaxAge = this._productService.quality !== 'stable' - ? 1000 * 60 * 60 * 24 * 7 // roughly 1 week - : 1000 * 60 * 60 * 24 * 30 * 3; // roughly 3 months + private readonly _DataMaxAge = this.productService.quality !== 'stable' + ? 1000 * 60 * 60 * 24 * 7 // roughly 1 week (insiders) + : 1000 * 60 * 60 * 24 * 30 * 3; // roughly 3 months (stable) constructor( - @INativeEnvironmentService private readonly _environmentService: INativeEnvironmentService, - @ILogService private readonly _logService: ILogService, - @IProductService private readonly _productService: IProductService + @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, + @ILogService private readonly logService: ILogService, + @IProductService private readonly productService: IProductService ) { super(); + // We have no Language pack support for dev version (run from source) // So only cleanup when we have a build version. - if (this._environmentService.isBuilt) { - this._manageCachedDataSoon(); + if (this.environmentService.isBuilt) { + const scheduler = this._register(new RunOnceScheduler(() => { + this.cleanUpLanguagePackCache(); + }, 40 * 1000 /* after 40s */)); + scheduler.schedule(); } } - private _manageCachedDataSoon(): void { - let handle: any = setTimeout(async () => { - handle = undefined; - this._logService.info('Starting to clean up unused language packs.'); - try { - const installed: IStringDictionary = Object.create(null); - 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; + private async cleanUpLanguagePackCache(): Promise { + this.logService.info('[language pack cache cleanup]: Starting to clean up unused language packs.'); + + try { + const installed: IStringDictionary = Object.create(null); + const metaData: ILanguagePackFile = JSON.parse(await Promises.readFile(join(this.environmentService.userDataPath, 'languagepacks.json'), 'utf8')); + for (let locale of Object.keys(metaData)) { + const entry = metaData[locale]; + installed[`${entry.hash}.${locale}`] = true; + } + + // Cleanup entries for language packs that aren't installed anymore + const cacheDir = join(this.environmentService.userDataPath, 'clp'); + const cacheDirExists = await Promises.exists(cacheDir); + if (!cacheDirExists) { + return; + } + + const entries = await Promises.readdir(cacheDir); + for (const entry of entries) { + if (installed[entry]) { + this.logService.info(`[language pack cache cleanup]: Skipping folder ${entry}. Language pack still in use.`); + continue; } - // Cleanup entries for language packs that aren't installed anymore - const cacheDir = path.join(this._environmentService.userDataPath, 'clp'); - const exists = await pfs.exists(cacheDir); - if (!exists) { - return; - } - for (let entry of await pfs.readdir(cacheDir)) { - if (installed[entry]) { - this._logService.info(`Skipping directory ${entry}. Language pack still in use.`); + + this.logService.info(`[language pack cache cleanup]: Removing unused language pack: ${entry}`); + + await Promises.rm(join(cacheDir, entry)); + } + + const now = Date.now(); + for (const packEntry of Object.keys(installed)) { + const folder = join(cacheDir, packEntry); + const entries = await Promises.readdir(folder); + for (const entry of entries) { + if (entry === 'tcf.json') { continue; } - this._logService.info('Removing unused language pack:', entry); - await pfs.rimraf(path.join(cacheDir, entry)); - } - const now = Date.now(); - for (let packEntry of Object.keys(installed)) { - const folder = path.join(cacheDir, packEntry); - for (let entry of await pfs.readdir(folder)) { - if (entry === 'tcf.json') { - continue; - } - const candidate = path.join(folder, entry); - const stat = await fs.promises.stat(candidate); - if (stat.isDirectory()) { - const diff = now - stat.mtime.getTime(); - if (diff > this._DataMaxAge) { - this._logService.info('Removing language pack cache entry: ', path.join(packEntry, entry)); - await pfs.rimraf(candidate); - } - } + const candidate = join(folder, entry); + const stat = await Promises.stat(candidate); + if (stat.isDirectory() && (now - stat.mtime.getTime()) > this._DataMaxAge) { + this.logService.info(`[language pack cache cleanup]: Removing language pack cache folder: ${join(packEntry, entry)}`); + + await Promises.rm(candidate); } } - } catch (error) { - onUnexpectedError(error); } - }, 40 * 1000); - - this._register(toDisposable(() => { - if (handle !== undefined) { - clearTimeout(handle); - } - })); + } catch (error) { + onUnexpectedError(error); + } } } diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner.ts index 903075e2b1..49f559e0ef 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner.ts @@ -5,42 +5,46 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { join, dirname, basename } from 'vs/base/common/path'; -import { readdir, rimraf } from 'vs/base/node/pfs'; +import { Promises } 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'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { ILogService } from 'vs/platform/log/common/log'; export class LogsDataCleaner extends Disposable { constructor( - @IEnvironmentService private readonly environmentService: IEnvironmentService + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @ILogService private readonly logService: ILogService ) { super(); - this.cleanUpOldLogsSoon(); + const scheduler = this._register(new RunOnceScheduler(() => { + this.cleanUpOldLogs(); + }, 10 * 1000 /* after 10s */)); + scheduler.schedule(); } - private cleanUpOldLogsSoon(): void { - let handle: NodeJS.Timeout | undefined = setTimeout(() => { - handle = undefined; + private async cleanUpOldLogs(): Promise { + this.logService.info('[logs cleanup]: Starting to clean up old logs.'); + try { const currentLog = basename(this.environmentService.logsPath); const logsRoot = dirname(this.environmentService.logsPath); - readdir(logsRoot).then(children => { - const allSessions = children.filter(name => /^\d{8}T\d{6}$/.test(name)); - const oldSessions = allSessions.sort().filter((d, i) => d !== currentLog); - const toDelete = oldSessions.slice(0, Math.max(0, oldSessions.length - 9)); + const logFiles = await Promises.readdir(logsRoot); - return Promises.settled(toDelete.map(name => rimraf(join(logsRoot, name)))); - }).then(null, onUnexpectedError); - }, 10 * 1000); + const allSessions = logFiles.filter(logFile => /^\d{8}T\d{6}$/.test(logFile)); + const oldSessions = allSessions.sort().filter(session => session !== currentLog); + const sessionsToDelete = oldSessions.slice(0, Math.max(0, oldSessions.length - 9)); - this._register(toDisposable(() => { - if (handle) { - clearTimeout(handle); - handle = undefined; + if (sessionsToDelete.length > 0) { + this.logService.info(`[logs cleanup]: Removing log folders '${sessionsToDelete.join(', ')}'`); + + await Promise.all(sessionsToDelete.map(sessionToDelete => Promises.rm(join(logsRoot, sessionToDelete)))); } - })); + } catch (error) { + onUnexpectedError(error); + } } } diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts deleted file mode 100644 index ef8bee1006..0000000000 --- a/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts +++ /dev/null @@ -1,87 +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 { 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 } from 'vs/base/node/pfs'; -import { IProductService } from 'vs/platform/product/common/productService'; - -export class NodeCachedDataCleaner { - - private readonly _DataMaxAge = this.productService.quality !== 'stable' - ? 1000 * 60 * 60 * 24 * 7 // roughly 1 week - : 1000 * 60 * 60 * 24 * 30 * 3; // roughly 3 months - - private readonly _disposables = new DisposableStore(); - - constructor( - private readonly nodeCachedDataDir: string | undefined, - @IProductService private readonly productService: IProductService - ) { - this._manageCachedDataSoon(); - } - - dispose(): void { - this._disposables.dispose(); - } - - private _manageCachedDataSoon(): void { - // Cached data is stored as user data and we run a cleanup task everytime - // the editor starts. The strategy is to delete all files that are older than - // 3 months (1 week respectively) - if (!this.nodeCachedDataDir) { - return; - } - - // The folder which contains folders of cached data. Each of these folder is per - // version - const nodeCachedDataRootDir = dirname(this.nodeCachedDataDir); - const nodeCachedDataCurrent = basename(this.nodeCachedDataDir); - - let handle: NodeJS.Timeout | undefined = setTimeout(() => { - handle = undefined; - - readdir(nodeCachedDataRootDir).then(entries => { - - const now = Date.now(); - const deletes: Promise[] = []; - - entries.forEach(entry => { - // name check - // * not the current cached data folder - if (entry !== nodeCachedDataCurrent) { - - const path = join(nodeCachedDataRootDir, entry); - deletes.push(promises.stat(path).then(stats => { - // stat check - // * only directories - // * only when old enough - if (stats.isDirectory()) { - const diff = now - stats.mtime.getTime(); - if (diff > this._DataMaxAge) { - return rimraf(path); - } - } - return undefined; - })); - } - }); - - return Promise.all(deletes); - - }).then(undefined, onUnexpectedError); - - }, 30 * 1000); - - this._disposables.add(toDisposable(() => { - if (handle) { - clearTimeout(handle); - handle = undefined; - } - })); - } -} diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts index 41001b5994..29d27a20f4 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts @@ -3,13 +3,14 @@ * 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, rimraf } from 'vs/base/node/pfs'; +import { Promises } from 'vs/base/node/pfs'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { IBackupWorkspacesFormat } from 'vs/platform/backup/node/backup'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { ILogService } from 'vs/platform/log/common/log'; export class StorageDataCleaner extends Disposable { @@ -18,52 +19,44 @@ export class StorageDataCleaner extends Disposable { constructor( private readonly backupWorkspacesPath: string, - @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService + @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, + @ILogService private readonly logService: ILogService ) { super(); - this.cleanUpStorageSoon(); + const scheduler = this._register(new RunOnceScheduler(() => { + this.cleanUpStorage(); + }, 30 * 1000 /* after 30s */)); + scheduler.schedule(); } - private cleanUpStorageSoon(): void { - let handle: NodeJS.Timeout | undefined = setTimeout(() => { - handle = undefined; + private async cleanUpStorage(): Promise { + this.logService.info('[storage cleanup]: Starting to clean up storage folders.'); - (async () => { - 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 promises.readFile(this.backupWorkspacesPath, 'utf8'); + try { - const workspaces = JSON.parse(contents) as IBackupWorkspacesFormat; - const emptyWorkspaces = workspaces.emptyWorkspaceInfos.map(info => info.backupFolder); + // 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 Promises.readFile(this.backupWorkspacesPath, 'utf8'); - // Read all workspace storage folders that exist - const storageFolders = await readdir(this.environmentService.workspaceStorageHome.fsPath); - const deletes: Promise[] = []; + const workspaces = JSON.parse(contents) as IBackupWorkspacesFormat; + const emptyWorkspaces = workspaces.emptyWorkspaceInfos.map(emptyWorkspace => emptyWorkspace.backupFolder); - storageFolders.forEach(storageFolder => { - if (storageFolder.length === StorageDataCleaner.NON_EMPTY_WORKSPACE_ID_LENGTH) { - return; - } - - if (emptyWorkspaces.indexOf(storageFolder) === -1) { - deletes.push(rimraf(join(this.environmentService.workspaceStorageHome.fsPath, storageFolder))); - } - }); - - await Promise.all(deletes); - } catch (error) { - onUnexpectedError(error); + // Read all workspace storage folders that exist + const storageFolders = await Promises.readdir(this.environmentService.workspaceStorageHome.fsPath); + await Promise.all(storageFolders.map(async storageFolder => { + if (storageFolder.length === StorageDataCleaner.NON_EMPTY_WORKSPACE_ID_LENGTH) { + return; } - })(); - }, 30 * 1000); - this._register(toDisposable(() => { - if (handle) { - clearTimeout(handle); - handle = undefined; - } - })); + if (emptyWorkspaces.indexOf(storageFolder) === -1) { + this.logService.info(`[storage cleanup]: Deleting storage folder ${storageFolder}.`); + + await Promises.rm(join(this.environmentService.workspaceStorageHome.fsPath, storageFolder)); + } + })); + } catch (error) { + onUnexpectedError(error); + } } } diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcess.js b/src/vs/code/electron-browser/sharedProcess/sharedProcess.js index 03d91a6bf3..66e5a0bdd2 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcess.js +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcess.js @@ -46,10 +46,7 @@ * forceEnableDeveloperKeybindings?: boolean, * disallowReloadKeybinding?: boolean, * removeDeveloperKeybindingsAfterLoad?: boolean - * }, - * canModifyDOM?: (config: ISandboxConfiguration) => void, - * beforeLoaderConfig?: (loaderConfig: object) => void, - * beforeRequire?: () => void + * } * } * ) => Promise * }} diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index ad36ddd9b1..2e499a27b4 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -36,7 +36,7 @@ import { ILocalizationsService } from 'vs/platform/localizations/common/localiza 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 { NodeCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner'; +import { CodeCacheCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/codeCacheCleaner'; 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'; @@ -77,7 +77,7 @@ import { LocalizationsUpdater } from 'vs/code/electron-browser/sharedProcess/con 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'; -import { TerminalIpcChannels } from 'vs/platform/terminal/common/terminal'; +import { LocalReconnectConstants, TerminalIpcChannels } from 'vs/platform/terminal/common/terminal'; import { PtyHostService } from 'vs/platform/terminal/node/ptyHostService'; import { ILocalPtyService } from 'vs/platform/terminal/electron-sandbox/terminal'; import { UserDataSyncChannel } from 'vs/platform/userDataSync/common/userDataSyncServiceIpc'; @@ -131,7 +131,7 @@ class SharedProcessMain extends Disposable { // Instantiate Contributions this._register(combinedDisposable( - instantiationService.createInstance(NodeCachedDataCleaner, this.configuration.nodeCachedDataDir), + instantiationService.createInstance(CodeCacheCleaner, this.configuration.codeCachePath), instantiationService.createInstance(LanguagePackCachedDataCleaner), instantiationService.createInstance(StorageDataCleaner, this.configuration.backupWorkspacesPath), instantiationService.createInstance(LogsDataCleaner), @@ -272,7 +272,19 @@ class SharedProcessMain extends Disposable { services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService)); // Terminal - services.set(ILocalPtyService, this._register(new PtyHostService(logService, telemetryService))); + services.set( + ILocalPtyService, + this._register( + new PtyHostService({ + GraceTime: LocalReconnectConstants.GraceTime, + ShortGraceTime: LocalReconnectConstants.ShortGraceTime + }, + configurationService, + logService, + telemetryService + ) + ) + ); return new InstantiationService(services); } diff --git a/src/vs/code/electron-browser/workbench/workbench.html b/src/vs/code/electron-browser/workbench/workbench.html index 008a296ece..5f36479b91 100644 --- a/src/vs/code/electron-browser/workbench/workbench.html +++ b/src/vs/code/electron-browser/workbench/workbench.html @@ -4,7 +4,7 @@ - + diff --git a/src/vs/code/electron-browser/workbench/workbench.js b/src/vs/code/electron-browser/workbench/workbench.js index ff85b140f1..226f5b0040 100644 --- a/src/vs/code/electron-browser/workbench/workbench.js +++ b/src/vs/code/electron-browser/workbench/workbench.js @@ -44,7 +44,7 @@ }; }, canModifyDOM: function (windowConfig) { - showPartsSplash(windowConfig); + showSplash(windowConfig); }, beforeLoaderConfig: function (loaderConfig) { loaderConfig.recordStats = true; @@ -90,19 +90,20 @@ /** * @typedef {import('../../../platform/windows/common/windows').INativeWindowConfiguration} INativeWindowConfiguration + * @typedef {import('../../../platform/environment/common/argv').NativeParsedArgs} NativeParsedArgs * * @returns {{ * load: ( * modules: string[], - * resultCallback: (result, configuration: INativeWindowConfiguration) => unknown, + * resultCallback: (result, configuration: INativeWindowConfiguration & NativeParsedArgs) => unknown, * options?: { - * configureDeveloperSettings?: (config: INativeWindowConfiguration & object) => { + * configureDeveloperSettings?: (config: INativeWindowConfiguration & NativeParsedArgs) => { * forceDisableShowDevtoolsOnError?: boolean, * forceEnableDeveloperKeybindings?: boolean, * disallowReloadKeybinding?: boolean, * removeDeveloperKeybindingsAfterLoad?: boolean * }, - * canModifyDOM?: (config: INativeWindowConfiguration & object) => void, + * canModifyDOM?: (config: INativeWindowConfiguration & NativeParsedArgs) => void, * beforeLoaderConfig?: (loaderConfig: object) => void, * beforeRequire?: () => void * } @@ -115,28 +116,15 @@ } /** - * @param {{ - * partsSplashPath?: string, - * colorScheme: ('light' | 'dark' | 'hc'), - * autoDetectHighContrast?: boolean, - * extensionDevelopmentPath?: string[], - * workspace?: import('../../../platform/workspaces/common/workspaces').IWorkspaceIdentifier | import('../../../platform/workspaces/common/workspaces').ISingleFolderWorkspaceIdentifier - * }} configuration + * @param {INativeWindowConfiguration & NativeParsedArgs} configuration */ - function showPartsSplash(configuration) { + function showSplash(configuration) { performance.mark('code/willShowPartsSplash'); - let data; - if (typeof configuration.partsSplashPath === 'string') { - try { - data = JSON.parse(require.__$__nodeRequire('fs').readFileSync(configuration.partsSplashPath, 'utf8')); - } catch (e) { - // ignore - } - } + let data = configuration.partsSplash; // high contrast mode has been turned on from the outside, e.g. OS -> ignore stored colors and layouts - const isHighContrast = configuration.colorScheme === 'hc' /* ColorScheme.HIGH_CONTRAST */ && configuration.autoDetectHighContrast; + const isHighContrast = configuration.colorScheme.highContrast && configuration.autoDetectHighContrast; if (data && isHighContrast && data.baseTheme !== 'hc-black') { data = undefined; } @@ -161,16 +149,18 @@ shellBackground = '#1E1E1E'; shellForeground = '#CCCCCC'; } + const style = document.createElement('style'); style.className = 'initialShellColors'; document.head.appendChild(style); style.textContent = `body { background-color: ${shellBackground}; color: ${shellForeground}; margin: 0; padding: 0; }`; - if (data && data.layoutInfo) { - // restore parts if possible (we might not always store layout info) - const { id, layoutInfo, colorInfo } = data; + // restore parts if possible (we might not always store layout info) + if (data?.layoutInfo) { + const { layoutInfo, colorInfo } = data; + const splash = document.createElement('div'); - splash.id = id; + splash.id = 'monaco-parts-splash'; splash.className = baseTheme; if (layoutInfo.windowBorder) { @@ -199,8 +189,8 @@ splash.appendChild(activityDiv); // part: side bar (only when opening workspace/folder) + // folder or workspace -> status bar color, sidebar 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};`); splash.appendChild(sideDiv); diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index e1ea344402..a489f3efd1 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -5,7 +5,7 @@ import { release, hostname } from 'os'; import { statSync } from 'fs'; -import { app, ipcMain, systemPreferences, contentTracing, protocol, BrowserWindow, dialog, session } from 'electron'; +import { app, ipcMain, systemPreferences, contentTracing, protocol, BrowserWindow, dialog, session, Session } from 'electron'; 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'; @@ -23,7 +23,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ILoggerService, ILogService } from 'vs/platform/log/common/log'; -import { IStateService } from 'vs/platform/state/node/state'; +import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IOpenURLOptions, IURLService } from 'vs/platform/url/common/url'; @@ -71,7 +71,7 @@ import { withNullAsUndefined } from 'vs/base/common/types'; import { mnemonicButtonLabel, getPathLabel } from 'vs/base/common/labels'; import { WebviewMainService } from 'vs/platform/webview/electron-main/webviewMainService'; import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService'; -import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { IFileService } from 'vs/platform/files/common/files'; import { stripComments } from 'vs/base/common/json'; import { generateUuid } from 'vs/base/common/uuid'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -81,12 +81,14 @@ import { IKeyboardLayoutMainService, KeyboardLayoutMainService } from 'vs/platfo import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper'; import { isEqualOrParent } from 'vs/base/common/extpath'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { RunOnceScheduler } from 'vs/base/common/async'; import { IExtensionUrlTrustService } from 'vs/platform/extensionManagement/common/extensionUrlTrust'; import { ExtensionUrlTrustService } from 'vs/platform/extensionManagement/node/extensionUrlTrustService'; import { once } from 'vs/base/common/functional'; import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; import { ISignService } from 'vs/platform/sign/common/sign'; +import { IExternalTerminalMainService } from 'vs/platform/externalTerminal/common/externalTerminal'; +import { LinuxExternalTerminalService, MacExternalTerminalService, WindowsExternalTerminalService } from 'vs/platform/externalTerminal/node/externalTerminalService'; /** * The main VS Code application. There will only ever be one instance, @@ -105,15 +107,66 @@ export class CodeApplication extends Disposable { @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IStateService private readonly stateService: IStateService, + @IStateMainService private readonly stateMainService: IStateMainService, @IFileService private readonly fileService: IFileService, @IProductService private readonly productService: IProductService ) { super(); + this.configureSession(); this.registerListeners(); } + private configureSession(): void { + + //#region Security related measures (https://electronjs.org/docs/tutorial/security) + // + // !!! DO NOT CHANGE without consulting the documentation !!! + // + + const isUrlFromWebview = (requestingUrl: string) => requestingUrl.startsWith(`${Schemas.vscodeWebview}://`); + + session.defaultSession.setPermissionRequestHandler((_webContents, permission /* 'media' | 'geolocation' | 'notifications' | 'midiSysex' | 'pointerLock' | 'fullscreen' | 'openExternal' */, callback, details) => { + if (isUrlFromWebview(details.requestingUrl)) { + return callback(permission === 'clipboard-read'); + } + + return callback(false); + }); + + session.defaultSession.setPermissionCheckHandler((_webContents, permission /* 'media' */, _origin, details) => { + if (isUrlFromWebview(details.requestingUrl)) { + return permission === 'clipboard-read'; + } + + return false; + }); + + //#endregion + + + //#region Code Cache + + type SessionWithCodeCachePathSupport = typeof Session & { + /** + * Sets code cache directory. By default, the directory will be `Code Cache` under + * the respective user data folder. + */ + setCodeCachePath?(path: string): void; + }; + + const defaultSession = session.defaultSession as unknown as SessionWithCodeCachePathSupport; + if (typeof defaultSession.setCodeCachePath === 'function' && this.environmentMainService.codeCachePath) { + // Make sure to partition Chrome's code cache folder + // in the same way as our code cache path to help + // invalidate caches that we know are invalid + // (https://github.com/microsoft/vscode/issues/120655) + defaultSession.setCodeCachePath(join(this.environmentMainService.codeCachePath, 'chrome')); + } + + //#endregion + } + private registerListeners(): void { // We handle uncaught exceptions here to prevent electron from opening a dialog to the user @@ -146,37 +199,6 @@ export class CodeApplication extends Disposable { // // !!! DO NOT CHANGE without consulting the documentation !!! // - app.on('remote-require', (event, sender, module) => { - this.logService.trace('app#on(remote-require): prevented'); - - event.preventDefault(); - }); - app.on('remote-get-global', (event, sender, 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}`); - - if (module !== 'clipboard') { - event.preventDefault(); - } - }); - app.on('remote-get-current-window', event => { - this.logService.trace(`app#on(remote-get-current-window): prevented`); - - event.preventDefault(); - }); - app.on('remote-get-current-web-contents', event => { - if (this.environmentMainService.args.driver) { - return; // the driver needs access to web contents - } - - this.logService.trace(`app#on(remote-get-current-web-contents): prevented`); - - event.preventDefault(); - }); app.on('web-contents-created', (event, contents) => { contents.on('will-attach-webview', (event, webPreferences, params) => { @@ -220,34 +242,17 @@ export class CodeApplication extends Disposable { event.preventDefault(); }); - contents.on('new-window', (event, url) => { - event.preventDefault(); // prevent code that wants to open links - + contents.setWindowOpenHandler(({ url }) => { this.nativeHostMainService?.openExternal(undefined, url); - }); - const isUrlFromWebview = (requestingUrl: string) => - requestingUrl.startsWith(`${Schemas.vscodeWebview}://`); - - session.defaultSession.setPermissionRequestHandler((_webContents, permission /* 'media' | 'geolocation' | 'notifications' | 'midiSysex' | 'pointerLock' | 'fullscreen' | 'openExternal' */, callback, details) => { - if (isUrlFromWebview(details.requestingUrl)) { - return callback(permission === 'clipboard-read'); - } - return callback(false); - }); - - session.defaultSession.setPermissionCheckHandler((_webContents, permission /* 'media' */, _origin, details) => { - if (isUrlFromWebview(details.requestingUrl)) { - return permission === 'clipboard-read'; - } - return false; + return { action: 'deny' }; }); }); //#endregion let macOpenFileURIs: IWindowOpenable[] = []; - let runningTimeout: NodeJS.Timeout | null = null; + let runningTimeout: NodeJS.Timeout | undefined = undefined; app.on('open-file', (event, path) => { this.logService.trace('app#open-file: ', path); event.preventDefault(); @@ -256,9 +261,9 @@ export class CodeApplication extends Disposable { macOpenFileURIs.push(this.getWindowOpenableFromPathSync(path)); // Clear previous handler if any - if (runningTimeout !== null) { + if (runningTimeout !== undefined) { clearTimeout(runningTimeout); - runningTimeout = null; + runningTimeout = undefined; } // Handle paths delayed in case more are coming! @@ -272,7 +277,7 @@ export class CodeApplication extends Disposable { }); macOpenFileURIs = []; - runningTimeout = null; + runningTimeout = undefined; }, 100); }); @@ -282,71 +287,29 @@ export class CodeApplication extends Disposable { //#region Bootstrap IPC Handlers - let slowShellResolveWarningShown = false; ipcMain.handle('vscode:fetchShellEnv', event => { - return new Promise(async resolve => { - // 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; + // 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 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! + const window = this.windowsMainService?.getWindowByWebContents(event.sender); // Note: this can be `undefined` for the shared process + let args: NativeParsedArgs; + let env: IProcessEnvironment; + if (window?.config) { + args = window.config; + env = { ...process.env, ...window.config.userEnv }; + } else { + args = this.environmentMainService.args; + env = process.env; + } - let replied = false; - - function acceptShellEnv(env: IProcessEnvironment): void { - clearTimeout(shellEnvSlowWarningHandle); - clearTimeout(shellEnvTimeoutErrorHandle); - - if (!replied) { - replied = true; - - if (!webContents.isDestroyed()) { - resolve(env); - } - } - } - - // Handle slow shell environment resolve calls: - // - 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(() => { - 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({}); - }, 10000); - - // 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 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: IProcessEnvironment; - if (window?.config) { - args = window.config; - env = { ...process.env, ...window.config.userEnv }; - } else { - args = this.environmentMainService.args; - env = process.env; - } - - // Resolve shell env - const shellEnv = await resolveShellEnv(this.logService, args, env); - acceptShellEnv(shellEnv); - }); + // Resolve shell env + return resolveShellEnv(this.logService, args, env); }); ipcMain.handle('vscode:writeNlsFile', (event, path: unknown, data: unknown) => { @@ -419,18 +382,6 @@ export class CodeApplication extends Disposable { this.logService.debug(`from: ${this.environmentMainService.appRoot}`); this.logService.debug('args:', this.environmentMainService.args); - // TODO@bpasero TODO@deepak1556 workaround for #120655 - try { - const cachedDataPath = URI.file(this.environmentMainService.chromeCachedDataDir); - this.logService.trace(`Deleting Chrome cached data path: ${cachedDataPath.fsPath}`); - - await this.fileService.del(cachedDataPath, { recursive: true }); - } catch (error) { - if ((error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) { - this.logService.error(error); - } - } - // Make sure we associate the program with the app user model id // This will help Windows to associate the running program with // any shortcut that is pinned to the taskbar and prevent showing @@ -498,11 +449,11 @@ export class CodeApplication extends Disposable { // We cache the machineId for faster lookups on startup // and resolve it only once initially if not cached or we need to replace the macOS iBridge device - let machineId = this.stateService.getItem(machineIdKey); + let machineId = this.stateMainService.getItem(machineIdKey); if (!machineId || (isMacintosh && machineId === '6c9d2bc8f91b89624add29c0abeae7fb42bf539fa1cdb2e3e57cd668fa9bcead')) { machineId = await getMachineId(); - this.stateService.setItem(machineIdKey, machineId); + this.stateMainService.setItem(machineIdKey, machineId); } return machineId; @@ -593,6 +544,15 @@ export class CodeApplication extends Disposable { // Storage services.set(IStorageMainService, new SyncDescriptor(StorageMainService)); + // External terminal + if (isWindows) { + services.set(IExternalTerminalMainService, new SyncDescriptor(WindowsExternalTerminalService)); + } else if (isMacintosh) { + services.set(IExternalTerminalMainService, new SyncDescriptor(MacExternalTerminalService)); + } else if (isLinux) { + services.set(IExternalTerminalMainService, new SyncDescriptor(LinuxExternalTerminalService)); + } + // Backups const backupMainService = new BackupMainService(this.environmentMainService, this.configurationService, this.logService); services.set(IBackupMainService, backupMainService); @@ -679,6 +639,10 @@ export class CodeApplication extends Disposable { mainProcessElectronServer.registerChannel('storage', storageChannel); sharedProcessClient.then(client => client.registerChannel('storage', storageChannel)); + // External Terminal + const externalTerminalChannel = ProxyChannel.fromService(accessor.get(IExternalTerminalMainService)); + mainProcessElectronServer.registerChannel('externalTerminal', externalTerminalChannel); + // Log Level (main & shared process) const logLevelChannel = new LogLevelChannel(accessor.get(ILogService)); mainProcessElectronServer.registerChannel('logLevel', logLevelChannel); @@ -705,16 +669,18 @@ export class CodeApplication extends Disposable { // Check for initial URLs to handle from protocol link invocations const pendingWindowOpenablesFromProtocolLinks: IWindowOpenable[] = []; const pendingProtocolLinksToHandle = [ + // Windows/Linux: protocol handler invokes CLI with --open-url ...this.environmentMainService.args['open-url'] ? this.environmentMainService.args._urls || [] : [], // macOS: open-url events ...((global).getOpenUrls() || []) as string[] + ].map(url => { try { return { uri: URI.parse(url), url }; } catch { - return null; + return undefined; } }).filter((obj): obj is { uri: URI, url: string } => { if (!obj) { @@ -744,8 +710,16 @@ export class CodeApplication extends Disposable { // protocol invocations outside of VSCode. const app = this; const environmentService = this.environmentMainService; + const productService = this.productService; urlService.registerHandler({ async handleURL(uri: URI, options?: IOpenURLOptions): Promise { + if (uri.scheme === productService.urlProtocol && uri.path === 'workspace') { + uri = uri.with({ + authority: 'file', + path: URI.parse(uri.query).path, + query: '' + }); + } // If URI should be blocked, behave as if it's handled if (app.shouldBlockURI(uri)) { @@ -760,7 +734,7 @@ export class CodeApplication extends Disposable { cli: { ...environmentService.args }, urisToOpen: [windowOpenableFromProtocolLink], gotoLineMode: true - /* remoteAuthority will be determined based on windowOpenableFromProtocolLink */ + // remoteAuthority: will be determined based on windowOpenableFromProtocolLink }); window.focus(); // this should help ensuring that the right window gets focus when multiple are opened @@ -823,7 +797,7 @@ export class CodeApplication extends Disposable { urisToOpen: pendingWindowOpenablesFromProtocolLinks, gotoLineMode: true, initialStartup: true - /* remoteAuthority will be determined based on pendingWindowOpenablesFromProtocolLinks */ + // remoteAuthority: will be determined based on pendingWindowOpenablesFromProtocolLinks }); } @@ -850,7 +824,7 @@ export class CodeApplication extends Disposable { noRecentEntry, waitMarkerFileURI, initialStartup: true, - /* remoteAuthority will be determined based on macOpenFiles */ + // remoteAuthority: will be determined based on macOpenFiles }); } @@ -957,10 +931,16 @@ export class CodeApplication extends Disposable { // Logging let message: string; - if (typeof details === 'string') { - message = details; - } else { - message = `SharedProcess: crashed (detail: ${details.reason})`; + switch (type) { + case WindowError.UNRESPONSIVE: + message = 'SharedProcess: detected unresponsive window'; + break; + case WindowError.CRASHED: + message = `SharedProcess: crashed (detail: ${details?.reason ?? ''}, code: ${details?.exitCode ?? ''})`; + break; + case WindowError.LOAD: + message = `SharedProcess: failed to load (detail: ${details?.reason ?? ''}, code: ${details?.exitCode ?? ''})`; + break; } onUnexpectedError(new Error(message)); @@ -968,18 +948,21 @@ export class CodeApplication extends Disposable { type SharedProcessErrorClassification = { type: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true }; reason: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true }; + code: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true }; visible: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true }; shuttingdown: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true }; }; type SharedProcessErrorEvent = { type: WindowError; reason: string | undefined; + code: number | undefined; visible: boolean; shuttingdown: boolean; }; telemetryService.publicLog2('sharedprocesserror', { type, - reason: typeof details !== 'string' ? details?.reason : undefined, + reason: details?.reason, + code: details?.exitCode, visible: sharedProcess.isVisible(), shuttingdown: willShutdown }); @@ -1012,7 +995,16 @@ export class CodeApplication extends Disposable { } // Start to fetch shell environment (if needed) after window has opened - resolveShellEnv(this.logService, this.environmentMainService.args, process.env); + // Since this operation can take a long time, we want to warm it up while + // the window is opening. + // We also print a warning if the resolution takes longer than 10s. + (async () => { + const slowResolveShellEnvWarning = this._register(new RunOnceScheduler(() => this.logService.warn('Resolving your shell environment is taking more than 10s. Please review your shell configuration. Learn more at https://go.microsoft.com/fwlink/?linkid=2149667.'), 10000)); + slowResolveShellEnvWarning.schedule(); + + await resolveShellEnv(this.logService, this.environmentMainService.args, process.env); + slowResolveShellEnvWarning.dispose(); + })(); // If enable-crash-reporter argv is undefined then this is a fresh start, // based on telemetry.enableCrashreporter settings, generate a UUID which diff --git a/src/vs/code/electron-main/auth.ts b/src/vs/code/electron-main/auth.ts index 6e1b15cc81..13f1c0e422 100644 --- a/src/vs/code/electron-main/auth.ts +++ b/src/vs/code/electron-main/auth.ts @@ -65,7 +65,6 @@ export class ProxyAuthHandler extends Disposable { private sessionCredentials: Credentials | undefined = undefined; constructor( - any, @ILogService private readonly logService: ILogService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 53641117b6..e5f983f77c 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -5,7 +5,8 @@ import 'vs/platform/update/common/update.config.contribution'; import { app, dialog } from 'electron'; -import { promises, unlinkSync } from 'fs'; +import { unlinkSync } from 'fs'; +import { Promises as FSPromises } from 'vs/base/node/pfs'; import { localize } from 'vs/nls'; import { isWindows, IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; import { mark } from 'vs/base/common/performance'; @@ -22,8 +23,8 @@ import { InstantiationService } from 'vs/platform/instantiation/common/instantia import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ILogService, ConsoleMainLogger, MultiplexLogService, getLogLevel, ILoggerService } from 'vs/platform/log/common/log'; -import { StateService } from 'vs/platform/state/node/stateService'; -import { IStateService } from 'vs/platform/state/node/state'; +import { StateMainService } from 'vs/platform/state/electron-main/stateMainService'; +import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; @@ -57,6 +58,7 @@ import { LoggerService } from 'vs/platform/log/node/loggerService'; import { cwd } from 'vs/base/common/process'; import { IProtocolMainService } from 'vs/platform/protocol/electron-main/protocol'; import { ProtocolMainService } from 'vs/platform/protocol/electron-main/protocolMainService'; +import { Promises } from 'vs/base/common/async'; /** * The main VS Code entry point. @@ -84,17 +86,17 @@ class CodeMain { setUnexpectedErrorHandler(err => console.error(err)); // Create services - const [instantiationService, instanceEnvironment, environmentService, configurationService, stateService, bufferLogService, productService] = this.createServices(); + const [instantiationService, instanceEnvironment, environmentMainService, configurationService, stateMainService, bufferLogService, productService] = this.createServices(); try { // Init services try { - await this.initServices(environmentService, configurationService, stateService); + await this.initServices(environmentMainService, configurationService, stateMainService); } catch (error) { // Show a dialog for errors that can be resolved by the user - this.handleStartupDataDirError(environmentService, productService.nameLong, error); + this.handleStartupDataDirError(environmentMainService, productService.nameLong, error); throw error; } @@ -108,10 +110,10 @@ class CodeMain { // Create the main IPC server by trying to be the server // If this throws an error it means we are not the first // instance of VS Code running and so we would quit. - const mainProcessNodeIpcServer = await this.claimInstance(logService, environmentService, lifecycleMainService, instantiationService, productService, true); + const mainProcessNodeIpcServer = await this.claimInstance(logService, environmentMainService, lifecycleMainService, instantiationService, productService, true); // Delay creation of spdlog for perf reasons (https://github.com/microsoft/vscode/issues/72906) - bufferLogService.logger = new SpdLogLogger('main', join(environmentService.logsPath, 'main.log'), true, bufferLogService.getLevel()); + bufferLogService.logger = new SpdLogLogger('main', join(environmentMainService.logsPath, 'main.log'), true, bufferLogService.getLevel()); // Lifecycle once(lifecycleMainService.onWillShutdown)(() => { @@ -126,7 +128,7 @@ class CodeMain { } } - private createServices(): [IInstantiationService, IProcessEnvironment, IEnvironmentMainService, ConfigurationService, StateService, BufferLogService, IProductService] { + private createServices(): [IInstantiationService, IProcessEnvironment, IEnvironmentMainService, ConfigurationService, StateMainService, BufferLogService, IProductService] { const services = new ServiceCollection(); // Product @@ -163,8 +165,8 @@ class CodeMain { services.set(ILifecycleMainService, new SyncDescriptor(LifecycleMainService)); // State - const stateService = new StateService(environmentMainService, logService); - services.set(IStateService, stateService); + const stateMainService = new StateMainService(environmentMainService, logService, fileService); + services.set(IStateMainService, stateMainService); // Request services.set(IRequestService, new SyncDescriptor(RequestMainService)); @@ -181,7 +183,7 @@ class CodeMain { // Protocol services.set(IProtocolMainService, new SyncDescriptor(ProtocolMainService)); - return [new InstantiationService(services, true), instanceEnvironment, environmentMainService, configurationService, stateService, bufferLogService, productService]; + return [new InstantiationService(services, true), instanceEnvironment, environmentMainService, configurationService, stateMainService, bufferLogService, productService]; } private patchEnvironment(environmentMainService: IEnvironmentMainService): IProcessEnvironment { @@ -201,25 +203,25 @@ class CodeMain { return instanceEnvironment; } - private initServices(environmentMainService: IEnvironmentMainService, configurationService: ConfigurationService, stateService: StateService): Promise { + private initServices(environmentMainService: IEnvironmentMainService, configurationService: ConfigurationService, stateMainService: StateMainService): Promise { + return Promises.settled([ - // Environment service (paths) - const environmentServiceInitialization = Promise.all([ - environmentMainService.extensionsPath, - environmentMainService.nodeCachedDataDir, - environmentMainService.logsPath, - environmentMainService.globalStorageHome.fsPath, - environmentMainService.workspaceStorageHome.fsPath, - environmentMainService.backupHome - ].map(path => path ? promises.mkdir(path, { recursive: true }) : undefined)); + // Environment service (paths) + Promise.all([ + environmentMainService.extensionsPath, + environmentMainService.codeCachePath, + environmentMainService.logsPath, + environmentMainService.globalStorageHome.fsPath, + environmentMainService.workspaceStorageHome.fsPath, + environmentMainService.backupHome + ].map(path => path ? FSPromises.mkdir(path, { recursive: true }) : undefined)), - // Configuration service - const configurationServiceInitialization = configurationService.initialize(); + // Configuration service + configurationService.initialize(), - // State service - const stateServiceInitialization = stateService.init(); - - return Promise.all([environmentServiceInitialization, configurationServiceInitialization, stateServiceInitialization]); + // State service + stateMainService.init() + ]); } private async claimInstance(logService: ILogService, environmentMainService: IEnvironmentMainService, lifecycleMainService: ILifecycleMainService, instantiationService: IInstantiationService, productService: IProductService, retry: boolean): Promise { diff --git a/src/vs/code/electron-sandbox/issue/issueReporter.js b/src/vs/code/electron-sandbox/issue/issueReporter.js index 22bf3b1bec..007bfeee9e 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporter.js +++ b/src/vs/code/electron-sandbox/issue/issueReporter.js @@ -35,10 +35,7 @@ * forceEnableDeveloperKeybindings?: boolean, * disallowReloadKeybinding?: boolean, * removeDeveloperKeybindingsAfterLoad?: boolean - * }, - * canModifyDOM?: (config: ISandboxConfiguration) => void, - * beforeLoaderConfig?: (loaderConfig: object) => void, - * beforeRequire?: () => void + * } * } * ) => Promise * }} diff --git a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts index ba6a287481..3fd2bfd1ac 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts @@ -11,6 +11,7 @@ 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 { Delayer } from 'vs/base/common/async'; import { groupBy } from 'vs/base/common/collections'; import { debounce } from 'vs/base/common/decorators'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -63,6 +64,7 @@ export class IssueReporter extends Disposable { private receivedPerformanceInfo = false; private shouldQueueSearch = false; private hasBeenSubmitted = false; + private delayedSubmit = new Delayer(300); private readonly previewButton!: Button; @@ -86,6 +88,7 @@ export class IssueReporter extends Disposable { const issueReporterElement = this.getElementById('issue-reporter'); if (issueReporterElement) { this.previewButton = new Button(issueReporterElement); + this.updatePreviewButtonState(); } const issueTitle = configuration.data.issueTitle; @@ -138,6 +141,7 @@ export class IssueReporter extends Disposable { this.applyStyles(configuration.data.styles); this.handleExtensionData(configuration.data.enabledExtensions); this.updateExperimentsInfo(configuration.data.experiments); + this.updateRestrictedMode(configuration.data.restrictedMode); } render(): void { @@ -356,7 +360,11 @@ export class IssueReporter extends Disposable { this.searchIssues(title, fileOnExtension, fileOnMarketplace); }); - this.previewButton.onDidClick(() => this.createIssue()); + this.previewButton.onDidClick(async () => { + this.delayedSubmit.trigger(async () => { + this.createIssue(); + }); + }); function sendWorkbenchCommand(commandId: string) { ipcRenderer.send('vscode:workbenchCommand', { id: commandId, from: 'issueReporter' }); @@ -383,9 +391,11 @@ export class IssueReporter extends Disposable { const cmdOrCtrlKey = isMacintosh ? e.metaKey : e.ctrlKey; // Cmd/Ctrl+Enter previews issue and closes window if (cmdOrCtrlKey && e.keyCode === 13) { - if (await this.createIssue()) { - ipcRenderer.send('vscode:closeIssueReporter'); - } + this.delayedSubmit.trigger(async () => { + if (await this.createIssue()) { + ipcRenderer.send('vscode:closeIssueReporter'); + } + }); } // Cmd/Ctrl + w closes issue window @@ -1151,6 +1161,10 @@ export class IssueReporter extends Disposable { } } + private updateRestrictedMode(restrictedMode: boolean) { + this.issueReporterModel.update({ restrictedMode }); + } + private updateExperimentsInfo(experimentInfo: string | undefined) { this.issueReporterModel.update({ experimentInfo }); const target = document.querySelector('.block-experiments .block-info'); diff --git a/src/vs/code/electron-sandbox/issue/issueReporterModel.ts b/src/vs/code/electron-sandbox/issue/issueReporterModel.ts index e227e5df82..601883c36e 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterModel.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterModel.ts @@ -32,6 +32,7 @@ export interface IssueReporterData { query?: string; filterResultCount?: number; experimentInfo?: string; + restrictedMode?: boolean; } export class IssueReporterModel { @@ -68,6 +69,7 @@ ${this._data.issueDescription} ${this.getExtensionVersion()} Azure Data Studio version: ${this._data.versionInfo && this._data.versionInfo.vscodeVersion} OS version: ${this._data.versionInfo && this._data.versionInfo.os} +Restricted Mode: ${this._data.restrictedMode ? 'Yes' : 'No'} ${this.getRemoteOSes()} ${this.getInfos()} `; diff --git a/src/vs/code/electron-sandbox/issue/test/testReporterModel.test.ts b/src/vs/code/electron-sandbox/issue/test/testReporterModel.test.ts index 7a56317db3..282dbfb5ff 100644 --- a/src/vs/code/electron-sandbox/issue/test/testReporterModel.test.ts +++ b/src/vs/code/electron-sandbox/issue/test/testReporterModel.test.ts @@ -33,6 +33,7 @@ undefined Azure Data Studio version: undefined OS version: undefined +Restricted Mode: No Extensions: none `); @@ -63,6 +64,7 @@ undefined Azure Data Studio version: undefined OS version: undefined +Restricted Mode: No

System Info @@ -106,6 +108,7 @@ undefined VS Code version: undefined OS version: undefined +Restricted Mode: No
System Info @@ -160,6 +163,7 @@ undefined VS Code version: undefined OS version: undefined +Restricted Mode: No
System Info @@ -216,6 +220,7 @@ undefined VS Code version: undefined OS version: undefined +Restricted Mode: No Remote OS version: Linux x64 4.18.0
@@ -264,6 +269,7 @@ undefined VS Code version: undefined OS version: undefined +Restricted Mode: No
System Info diff --git a/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css b/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css index ccbccd94f3..fc21e0d5b9 100644 --- a/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css +++ b/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css @@ -49,6 +49,10 @@ body { width: 90px; } +.monaco-list:focus { + outline: 0; +} + .monaco-list-row:first-of-type { border-bottom: 1px solid; } diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorer.js b/src/vs/code/electron-sandbox/processExplorer/processExplorer.js index 2026d8e980..b9ab7dde0a 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorer.js +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorer.js @@ -32,10 +32,7 @@ * forceEnableDeveloperKeybindings?: boolean, * disallowReloadKeybinding?: boolean, * removeDeveloperKeybindingsAfterLoad?: boolean - * }, - * canModifyDOM?: (config: ISandboxConfiguration) => void, - * beforeLoaderConfig?: (loaderConfig: object) => void, - * beforeRequire?: () => void + * } * } * ) => Promise * }} diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts b/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts index 9924a9233d..383aa8ef80 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts @@ -14,13 +14,15 @@ 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 { append, $ } from 'vs/base/browser/dom'; +import { append, $, createStyleSheet } from 'vs/base/browser/dom'; import { isRemoteDiagnosticError, IRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; 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'; +import { getIconsStyleSheet } from 'vs/platform/theme/browser/iconsStyleSheet'; +import { RunOnceScheduler } from 'vs/base/common/async'; const DEBUG_FLAGS_PATTERN = /\s--(inspect|debug)(-brk|port)?=(\d+)?/; const DEBUG_PORT_PATTERN = /\s--(inspect|debug)-port=(\d+)/; @@ -310,8 +312,7 @@ class ProcessExplorer { renderers, new ProcessTreeDataSource(), { - identityProvider: - { + identityProvider: { getId: (element: ProcessTree | ProcessItem | MachineProcessInformation | ProcessInformation | IRemoteDiagnosticError) => { if (isProcessItem(element)) { return element.pid.toString(); @@ -331,7 +332,7 @@ class ProcessExplorer { return 'header'; } - } + }, }); this.tree.setInput({ processes: { processRoots } }); @@ -378,21 +379,45 @@ class ProcessExplorer { } private applyStyles(styles: ProcessExplorerStyles): void { - const styleTag = document.createElement('style'); + const styleElement = createStyleSheet(); const content: string[] = []; - if (styles.hoverBackground) { - content.push(`.monaco-list-row:hover { background-color: ${styles.hoverBackground}; }`); + if (styles.listFocusBackground) { + content.push(`.monaco-list:focus .monaco-list-row.focused { background-color: ${styles.listFocusBackground}; }`); + content.push(`.monaco-list:focus .monaco-list-row.focused:hover { background-color: ${styles.listFocusBackground}; }`); } - if (styles.hoverForeground) { - content.push(`.monaco-list-row:hover { color: ${styles.hoverForeground}; }`); + if (styles.listFocusForeground) { + content.push(`.monaco-list:focus .monaco-list-row.focused { color: ${styles.listFocusForeground}; }`); } - styleTag.textContent = content.join('\n'); - if (document.head) { - document.head.appendChild(styleTag); + if (styles.listActiveSelectionBackground) { + content.push(`.monaco-list:focus .monaco-list-row.selected { background-color: ${styles.listActiveSelectionBackground}; }`); + content.push(`.monaco-list:focus .monaco-list-row.selected:hover { background-color: ${styles.listActiveSelectionBackground}; }`); } + + if (styles.listActiveSelectionForeground) { + content.push(`.monaco-list:focus .monaco-list-row.selected { color: ${styles.listActiveSelectionForeground}; }`); + } + + if (styles.listHoverBackground) { + content.push(`.monaco-list-row:hover:not(.selected):not(.focused) { background-color: ${styles.listHoverBackground}; }`); + } + + if (styles.listHoverForeground) { + content.push(`.monaco-list-row:hover:not(.selected):not(.focused) { color: ${styles.listHoverForeground}; }`); + } + + if (styles.listFocusOutline) { + content.push(`.monaco-list:focus .monaco-list-row.focused { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; }`); + } + + if (styles.listHoverOutline) { + content.push(`.monaco-list-row:hover { outline: 1px dashed ${styles.listHoverOutline}; outline-offset: -1px; }`); + } + + styleElement.textContent = content.join('\n'); + if (styles.color) { document.body.style.color = styles.color; } @@ -475,9 +500,24 @@ class ProcessExplorer { } } +function createCodiconStyleSheet() { + const codiconStyleSheet = createStyleSheet(); + codiconStyleSheet.id = 'codiconStyles'; + + const iconsStyleSheet = getIconsStyleSheet(); + function updateAll() { + codiconStyleSheet.textContent = iconsStyleSheet.getCSS(); + } + + const delayer = new RunOnceScheduler(updateAll, 0); + iconsStyleSheet.onDidChange(() => delayer.schedule()); + delayer.schedule(); +} + export function startup(configuration: ProcessExplorerWindowConfiguration): void { const platformClass = configuration.data.platform === 'win32' ? 'windows' : configuration.data.platform === 'linux' ? 'linux' : 'mac'; document.body.classList.add(platformClass); // used by our fonts + createCodiconStyleSheet(); applyZoom(configuration.data.zoomLevel); new ProcessExplorer(configuration.windowId, configuration.data); diff --git a/src/vs/code/electron-sandbox/workbench/workbench.html b/src/vs/code/electron-sandbox/workbench/workbench.html index 2933be2736..38405e41e1 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.html +++ b/src/vs/code/electron-sandbox/workbench/workbench.html @@ -4,7 +4,7 @@ - + diff --git a/src/vs/code/electron-sandbox/workbench/workbench.js b/src/vs/code/electron-sandbox/workbench/workbench.js index 1504b3f181..3dc73ce3cc 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.js +++ b/src/vs/code/electron-sandbox/workbench/workbench.js @@ -43,7 +43,7 @@ }; }, canModifyDOM: function (windowConfig) { - // TODO@sandbox part-splash is non-sandboxed only + showSplash(windowConfig); }, beforeLoaderConfig: function (loaderConfig) { loaderConfig.recordStats = true; @@ -89,18 +89,20 @@ /** * @typedef {import('../../../platform/windows/common/windows').INativeWindowConfiguration} INativeWindowConfiguration + * @typedef {import('../../../platform/environment/common/argv').NativeParsedArgs} NativeParsedArgs * * @returns {{ * load: ( * modules: string[], - * resultCallback: (result, configuration: INativeWindowConfiguration) => unknown, + * resultCallback: (result, configuration: INativeWindowConfiguration & NativeParsedArgs) => unknown, * options?: { - * configureDeveloperSettings?: (config: INativeWindowConfiguration & object) => { + * configureDeveloperSettings?: (config: INativeWindowConfiguration & NativeParsedArgs) => { + * forceDisableShowDevtoolsOnError?: boolean, * forceEnableDeveloperKeybindings?: boolean, * disallowReloadKeybinding?: boolean, * removeDeveloperKeybindingsAfterLoad?: boolean * }, - * canModifyDOM?: (config: INativeWindowConfiguration & object) => void, + * canModifyDOM?: (config: INativeWindowConfiguration & NativeParsedArgs) => void, * beforeLoaderConfig?: (loaderConfig: object) => void, * beforeRequire?: () => void * } @@ -112,5 +114,97 @@ return window.MonacoBootstrapWindow; } + /** + * @param {INativeWindowConfiguration & NativeParsedArgs} configuration + */ + function showSplash(configuration) { + performance.mark('code/willShowPartsSplash'); + + let data = configuration.partsSplash; + + // high contrast mode has been turned on from the outside, e.g. OS -> ignore stored colors and layouts + const isHighContrast = configuration.colorScheme.highContrast && configuration.autoDetectHighContrast; + if (data && isHighContrast && data.baseTheme !== 'hc-black') { + data = undefined; + } + + // developing an extension -> ignore stored layouts + if (data && configuration.extensionDevelopmentPath) { + data.layoutInfo = undefined; + } + + // minimal color configuration (works with or without persisted data) + let baseTheme, shellBackground, shellForeground; + if (data) { + baseTheme = data.baseTheme; + shellBackground = data.colorInfo.editorBackground; + shellForeground = data.colorInfo.foreground; + } else if (isHighContrast) { + baseTheme = 'hc-black'; + shellBackground = '#000000'; + shellForeground = '#FFFFFF'; + } else { + baseTheme = 'vs-dark'; + shellBackground = '#1E1E1E'; + shellForeground = '#CCCCCC'; + } + + const style = document.createElement('style'); + style.className = 'initialShellColors'; + document.head.appendChild(style); + style.textContent = `body { background-color: ${shellBackground}; color: ${shellForeground}; margin: 0; padding: 0; }`; + + // restore parts if possible (we might not always store layout info) + if (data?.layoutInfo) { + const { layoutInfo, colorInfo } = data; + + const splash = document.createElement('div'); + splash.id = 'monaco-parts-splash'; + splash.className = baseTheme; + + if (layoutInfo.windowBorder) { + splash.style.position = 'relative'; + splash.style.height = 'calc(100vh - 2px)'; + splash.style.width = 'calc(100vw - 2px)'; + splash.style.border = '1px solid var(--window-border-color)'; + splash.style.setProperty('--window-border-color', colorInfo.windowBorder); + + if (layoutInfo.windowBorderRadius) { + splash.style.borderRadius = layoutInfo.windowBorderRadius; + } + } + + // ensure there is enough space + layoutInfo.sideBarWidth = Math.min(layoutInfo.sideBarWidth, window.innerWidth - (layoutInfo.activityBarWidth + layoutInfo.editorPartMinWidth)); + + // part: title + const titleDiv = document.createElement('div'); + titleDiv.setAttribute('style', `position: absolute; width: 100%; left: 0; top: 0; height: ${layoutInfo.titleBarHeight}px; background-color: ${colorInfo.titleBarBackground}; -webkit-app-region: drag;`); + splash.appendChild(titleDiv); + + // part: activity bar + const activityDiv = document.createElement('div'); + activityDiv.setAttribute('style', `position: absolute; height: calc(100% - ${layoutInfo.titleBarHeight}px); top: ${layoutInfo.titleBarHeight}px; ${layoutInfo.sideBarSide}: 0; width: ${layoutInfo.activityBarWidth}px; background-color: ${colorInfo.activityBarBackground};`); + splash.appendChild(activityDiv); + + // part: side bar (only when opening workspace/folder) + // folder or workspace -> status bar color, sidebar + if (configuration.workspace) { + 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};`); + splash.appendChild(sideDiv); + } + + // 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.workspace ? colorInfo.statusBarBackground : colorInfo.statusBarNoFolderBackground};`); + splash.appendChild(statusDiv); + + document.body.appendChild(splash); + } + + performance.mark('code/didShowPartsSplash'); + } + //#endregion }()); diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 6648b73e89..06c81b3b3c 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -13,8 +13,9 @@ import { createWaitMarkerFile } from 'vs/platform/environment/node/wait'; import product from 'vs/platform/product/common/product'; 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, IProcessEnvironment } from 'vs/base/common/platform'; +import { findFreePort } from 'vs/base/node/ports'; +import { randomPort } from 'vs/base/common/ports'; +import { isWindows, IProcessEnvironment } from 'vs/base/common/platform'; import type { ProfilingSession, Target } from 'v8-inspect-profiler'; import { isString } from 'vs/base/common/types'; import { hasStdinWithoutTty, stdinDataListener, getStdinFilePath, readFromStdin } from 'vs/platform/environment/node/stdin'; @@ -55,7 +56,7 @@ export async function main(argv: string[]): Promise { // Extensions Management else if (shouldSpawnCliProcess(args)) { - const cli = await new Promise((c, e) => require(['vs/code/node/cliProcessMain'], c, e)); + const cli = await new Promise((resolve, reject) => require(['vs/code/node/cliProcessMain'], resolve, reject)); await cli.main(args); return; @@ -318,10 +319,6 @@ export async function main(argv: string[]): Promise { options['stdio'] = 'ignore'; } - if (isLinux) { - addArg(argv, '--no-sandbox'); // Electron 6 introduces a chrome-sandbox that requires root to run. This can fail. Disable sandbox via --no-sandbox - } - const child = spawn(process.execPath, argv.slice(2), options); if (args.wait && waitMarkerFilePath) { diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index dc6ed8b3a8..ec96feaa80 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -6,6 +6,7 @@ import { release, hostname } from 'os'; import * as fs from 'fs'; import { gracefulify } from 'graceful-fs'; +import { Promises } from 'vs/base/node/pfs'; import { isAbsolute, join } from 'vs/base/common/path'; import { raceTimeout } from 'vs/base/common/async'; import product from 'vs/platform/product/common/product'; @@ -19,7 +20,7 @@ import { NativeEnvironmentService } from 'vs/platform/environment/node/environme 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 { ITelemetryService, machineIdKey } 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/common/commonProperties'; @@ -28,8 +29,6 @@ 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 { IStateService } from 'vs/platform/state/node/state'; -import { StateService } from 'vs/platform/state/node/stateService'; import { ILogService, getLogLevel, LogLevel, ConsoleLogger, MultiplexLogService, ILogger } from 'vs/platform/log/common/log'; import { Schemas } from 'vs/base/common/network'; import { SpdLogLogger } from 'vs/platform/log/node/spdlogLog'; @@ -104,7 +103,7 @@ class CliMain extends Disposable { services.set(INativeEnvironmentService, environmentService); // Init folders - await Promise.all([environmentService.appSettingsHome.fsPath, environmentService.extensionsPath].map(path => path ? fs.promises.mkdir(path, { recursive: true }) : undefined)); + await Promise.all([environmentService.appSettingsHome.fsPath, environmentService.extensionsPath].map(path => path ? Promises.mkdir(path, { recursive: true }) : undefined)); // Log const logLevel = getLogLevel(environmentService); @@ -131,10 +130,6 @@ class CliMain extends Disposable { // Init config await configurationService.initialize(); - // State - const stateService = new StateService(environmentService, logService); - services.set(IStateService, stateService); - // Request services.set(IRequestService, new SyncDescriptor(RequestService)); @@ -158,7 +153,19 @@ class CliMain extends Disposable { const config: ITelemetryServiceConfig = { appender: combinedAppender(...appenders), sendErrorTelemetry: false, - commonProperties: resolveCommonProperties(fileService, release(), hostname(), process.arch, productService.commit, productService.version, stateService.getItem('telemetry.machineId'), productService.msftInternalDomains, installSourcePath), + commonProperties: (async () => { + let machineId: string | undefined = undefined; + try { + const storageContents = await Promises.readFile(join(environmentService.userDataPath, 'storage.json')); + machineId = JSON.parse(storageContents.toString())[machineIdKey]; + } catch (error) { + if (error.code !== 'ENOENT') { + logService.error(error); + } + } + + return resolveCommonProperties(fileService, release(), hostname(), process.arch, productService.commit, productService.version, machineId, productService.msftInternalDomains, installSourcePath); + })(), piiPaths: [appRoot, extensionsPath] }; @@ -213,7 +220,7 @@ class CliMain extends Disposable { // Telemetry else if (this.argv['telemetry']) { - console.log(buildTelemetryMessage(environmentService.appRoot, environmentService.extensionsPath)); + console.log(await buildTelemetryMessage(environmentService.appRoot, environmentService.extensionsPath)); } } diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index 90bc8c8381..e462197661 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -40,54 +40,40 @@ export interface IEmptyContentData { horizontalDistanceToText?: number; } -interface IETextRange { - boundingHeight: number; - boundingLeft: number; - boundingTop: number; - boundingWidth: number; - htmlText: string; - offsetLeft: number; - offsetTop: number; - text: string; - collapse(start?: boolean): void; - compareEndPoints(how: string, sourceRange: IETextRange): number; - duplicate(): IETextRange; - execCommand(cmdID: string, showUI?: boolean, value?: any): boolean; - execCommandShowHelp(cmdID: string): boolean; - expand(Unit: string): boolean; - findText(string: string, count?: number, flags?: number): boolean; - getBookmark(): string; - getBoundingClientRect(): ClientRect; - getClientRects(): ClientRectList; - inRange(range: IETextRange): boolean; - isEqual(range: IETextRange): boolean; - move(unit: string, count?: number): number; - moveEnd(unit: string, count?: number): number; - moveStart(unit: string, count?: number): number; - moveToBookmark(bookmark: string): boolean; - moveToElementText(element: Element): void; - moveToPoint(x: number, y: number): void; - parentElement(): Element; - pasteHTML(html: string): void; - queryCommandEnabled(cmdID: string): boolean; - queryCommandIndeterm(cmdID: string): boolean; - queryCommandState(cmdID: string): boolean; - queryCommandSupported(cmdID: string): boolean; - queryCommandText(cmdID: string): string; - queryCommandValue(cmdID: string): any; - scrollIntoView(fStart?: boolean): void; - select(): void; - setEndPoint(how: string, SourceRange: IETextRange): void; +export interface ITextContentData { + mightBeForeignElement: boolean; } -declare const IETextRange: { - prototype: IETextRange; - new(): IETextRange; -}; +const enum HitTestResultType { + Unknown = 0, + Content = 1, +} -interface IHitTestResult { - position: Position | null; - hitTarget: Element | null; +class UnknownHitTestResult { + readonly type = HitTestResultType.Unknown; + constructor( + readonly hitTarget: Element | null = null + ) { } +} + +class ContentHitTestResult { + readonly type = HitTestResultType.Content; + constructor( + readonly position: Position, + readonly spanNode: HTMLElement + ) { } +} + +type HitTestResult = UnknownHitTestResult | ContentHitTestResult; + +namespace HitTestResult { + export function createFromDOMInfo(ctx: HitTestContext, spanNode: HTMLElement, offset: number): HitTestResult { + const position = ctx.getPositionFromDOMInfo(spanNode, offset); + if (position) { + return new ContentHitTestResult(position, spanNode); + } + return new UnknownHitTestResult(spanNode); + } } export class PointerHandlerLastRenderData { @@ -426,6 +412,17 @@ class HitTestRequest extends BareHitTestRequest { return `pos(${this.pos.x},${this.pos.y}), editorPos(${this.editorPos.x},${this.editorPos.y}), mouseVerticalOffset: ${this.mouseVerticalOffset}, mouseContentHorizontalOffset: ${this.mouseContentHorizontalOffset}\n\ttarget: ${this.target ? (this.target).outerHTML : null}`; } + public fulfill(type: MouseTargetType.UNKNOWN, position?: Position | null, range?: EditorRange | null): MouseTarget; + public fulfill(type: MouseTargetType.TEXTAREA, position: Position | null): MouseTarget; + public fulfill(type: MouseTargetType.GUTTER_GLYPH_MARGIN | MouseTargetType.GUTTER_LINE_NUMBERS | MouseTargetType.GUTTER_LINE_DECORATIONS, position: Position, range: EditorRange, detail: IMarginData): MouseTarget; + public fulfill(type: MouseTargetType.GUTTER_VIEW_ZONE | MouseTargetType.CONTENT_VIEW_ZONE, position: Position, range: null, detail: IViewZoneData): MouseTarget; + public fulfill(type: MouseTargetType.CONTENT_TEXT, position: Position | null, range: EditorRange | null, detail: ITextContentData): MouseTarget; + public fulfill(type: MouseTargetType.CONTENT_EMPTY, position: Position | null, range: EditorRange | null, detail: IEmptyContentData): MouseTarget; + public fulfill(type: MouseTargetType.CONTENT_WIDGET, position: null, range: null, detail: string): MouseTarget; + public fulfill(type: MouseTargetType.SCROLLBAR, position: Position): MouseTarget; + public fulfill(type: MouseTargetType.OVERLAY_WIDGET, position: null, range: null, detail: string): MouseTarget; + // public fulfill(type: MouseTargetType.OVERVIEW_RULER, position?: Position | null, range?: EditorRange | null, detail?: any): MouseTarget; + // public fulfill(type: MouseTargetType.OUTSIDE_EDITOR, position?: Position | null, range?: EditorRange | null, detail?: any): MouseTarget; public fulfill(type: MouseTargetType, position: Position | null = null, range: EditorRange | null = null, detail: any = null): MouseTarget { let mouseColumn = this.mouseColumn; if (position && position.column < this._ctx.model.getLineMaxColumn(position.lineNumber)) { @@ -506,8 +503,8 @@ export class MouseTargetFactory { const hitTestResult = MouseTargetFactory._doHitTest(ctx, request); - if (hitTestResult.position) { - return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.position.lineNumber, hitTestResult.position.column); + if (hitTestResult.type === HitTestResultType.Content) { + return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.spanNode, hitTestResult.position); } return this._createMouseTarget(ctx, request.withTarget(hitTestResult.hitTarget), true); @@ -567,7 +564,7 @@ export class MouseTargetFactory { for (const d of lastViewCursorsRenderData) { if (request.target === d.domNode) { - return request.fulfill(MouseTargetType.CONTENT_TEXT, d.position); + return request.fulfill(MouseTargetType.CONTENT_TEXT, d.position, null, { mightBeForeignElement: false }); } } } @@ -599,7 +596,7 @@ export class MouseTargetFactory { cursorVerticalOffset <= mouseVerticalOffset && mouseVerticalOffset <= cursorVerticalOffset + d.height ) { - return request.fulfill(MouseTargetType.CONTENT_TEXT, d.position); + return request.fulfill(MouseTargetType.CONTENT_TEXT, d.position, null, { mightBeForeignElement: false }); } } } @@ -621,7 +618,7 @@ export class MouseTargetFactory { // Is it the textarea? if (ElementPath.isTextArea(request.targetPath)) { if (ctx.lastRenderData.lastTextareaPosition) { - return request.fulfill(MouseTargetType.CONTENT_TEXT, ctx.lastRenderData.lastTextareaPosition); + return request.fulfill(MouseTargetType.CONTENT_TEXT, ctx.lastRenderData.lastTextareaPosition, null, { mightBeForeignElement: false }); } return request.fulfill(MouseTargetType.TEXTAREA, ctx.lastRenderData.lastTextareaPosition); } @@ -667,7 +664,7 @@ export class MouseTargetFactory { } if (ctx.isInTopPadding(request.mouseVerticalOffset)) { - return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(1, 1), undefined, EMPTY_CONTENT_AFTER_LINES); + return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(1, 1), null, EMPTY_CONTENT_AFTER_LINES); } // Check if it is below any lines and any view zones @@ -675,7 +672,7 @@ export class MouseTargetFactory { // This most likely indicates it happened after the last view-line const lineCount = ctx.model.getLineCount(); const maxLineColumn = ctx.model.getLineMaxColumn(lineCount); - return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(lineCount, maxLineColumn), undefined, EMPTY_CONTENT_AFTER_LINES); + return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(lineCount, maxLineColumn), null, EMPTY_CONTENT_AFTER_LINES); } if (domHitTestExecuted) { @@ -686,14 +683,14 @@ export class MouseTargetFactory { if (ctx.model.getLineLength(lineNumber) === 0) { const lineWidth = ctx.getLineWidth(lineNumber); const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth); - return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(lineNumber, 1), undefined, detail); + return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(lineNumber, 1), null, detail); } const lineWidth = ctx.getLineWidth(lineNumber); if (request.mouseContentHorizontalOffset >= lineWidth) { const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth); const pos = new Position(lineNumber, ctx.model.getLineMaxColumn(lineNumber)); - return request.fulfill(MouseTargetType.CONTENT_EMPTY, pos, undefined, detail); + return request.fulfill(MouseTargetType.CONTENT_EMPTY, pos, null, detail); } } @@ -703,8 +700,8 @@ export class MouseTargetFactory { const hitTestResult = MouseTargetFactory._doHitTest(ctx, request); - if (hitTestResult.position) { - return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.position.lineNumber, hitTestResult.position.column); + if (hitTestResult.type === HitTestResultType.Content) { + return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.spanNode, hitTestResult.position); } return this._createMouseTarget(ctx, request.withTarget(hitTestResult.hitTarget), true); @@ -760,14 +757,15 @@ export class MouseTargetFactory { return (chars + 1); } - private static createMouseTargetFromHitTestPosition(ctx: HitTestContext, request: HitTestRequest, lineNumber: number, column: number): MouseTarget { - const pos = new Position(lineNumber, column); + private static createMouseTargetFromHitTestPosition(ctx: HitTestContext, request: HitTestRequest, spanNode: HTMLElement, pos: Position): MouseTarget { + const lineNumber = pos.lineNumber; + const column = pos.column; const lineWidth = ctx.getLineWidth(lineNumber); if (request.mouseContentHorizontalOffset > lineWidth) { const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth); - return request.fulfill(MouseTargetType.CONTENT_EMPTY, pos, undefined, detail); + return request.fulfill(MouseTargetType.CONTENT_EMPTY, pos, null, detail); } const visibleRange = ctx.visibleRangeForPosition(lineNumber, column); @@ -779,7 +777,7 @@ export class MouseTargetFactory { const columnHorizontalOffset = visibleRange.left; if (request.mouseContentHorizontalOffset === columnHorizontalOffset) { - return request.fulfill(MouseTargetType.CONTENT_TEXT, pos); + return request.fulfill(MouseTargetType.CONTENT_TEXT, pos, null, { mightBeForeignElement: false }); } // Let's define a, b, c and check if the offset is in between them... @@ -803,21 +801,25 @@ export class MouseTargetFactory { points.sort((a, b) => a.offset - b.offset); + const mouseCoordinates = request.pos.toClientCoordinates(); + const spanNodeClientRect = spanNode.getBoundingClientRect(); + const mouseIsOverSpanNode = (spanNodeClientRect.left <= mouseCoordinates.clientX && mouseCoordinates.clientX <= spanNodeClientRect.right); + for (let i = 1; i < points.length; i++) { const prev = points[i - 1]; const curr = points[i]; if (prev.offset <= request.mouseContentHorizontalOffset && request.mouseContentHorizontalOffset <= curr.offset) { const rng = new EditorRange(lineNumber, prev.column, lineNumber, curr.column); - return request.fulfill(MouseTargetType.CONTENT_TEXT, pos, rng); + return request.fulfill(MouseTargetType.CONTENT_TEXT, pos, rng, { mightBeForeignElement: !mouseIsOverSpanNode }); } } - return request.fulfill(MouseTargetType.CONTENT_TEXT, pos); + return request.fulfill(MouseTargetType.CONTENT_TEXT, pos, null, { mightBeForeignElement: !mouseIsOverSpanNode }); } /** * Most probably WebKit browsers and Edge */ - private static _doHitTestWithCaretRangeFromPoint(ctx: HitTestContext, request: BareHitTestRequest): IHitTestResult { + private static _doHitTestWithCaretRangeFromPoint(ctx: HitTestContext, request: BareHitTestRequest): HitTestResult { // In Chrome, especially on Linux it is possible to click between lines, // so try to adjust the `hity` below so that it lands in the center of a line @@ -836,7 +838,7 @@ export class MouseTargetFactory { const adjustedPage = new PageCoordinates(request.pos.x, adjustedPageY); const r = this._actualDoHitTestWithCaretRangeFromPoint(ctx, adjustedPage.toClientCoordinates()); - if (r.position) { + if (r.type === HitTestResultType.Content) { return r; } @@ -844,7 +846,7 @@ export class MouseTargetFactory { return this._actualDoHitTestWithCaretRangeFromPoint(ctx, request.pos.toClientCoordinates()); } - private static _actualDoHitTestWithCaretRangeFromPoint(ctx: HitTestContext, coords: ClientCoordinates): IHitTestResult { + private static _actualDoHitTestWithCaretRangeFromPoint(ctx: HitTestContext, coords: ClientCoordinates): HitTestResult { const shadowRoot = dom.getShadowRoot(ctx.viewDomNode); let range: Range; if (shadowRoot) { @@ -858,15 +860,11 @@ export class MouseTargetFactory { } if (!range || !range.startContainer) { - return { - position: null, - hitTarget: null - }; + return new UnknownHitTestResult(); } // Chrome always hits a TEXT_NODE, while Edge sometimes hits a token span const startContainer = range.startContainer; - let hitTarget: HTMLElement | null = null; if (startContainer.nodeType === startContainer.TEXT_NODE) { // startContainer is expected to be the token text @@ -876,13 +874,9 @@ export class MouseTargetFactory { const parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? (parent3).className : null; if (parent3ClassName === ViewLine.CLASS_NAME) { - const p = ctx.getPositionFromDOMInfo(parent1, range.startOffset); - return { - position: p, - hitTarget: null - }; + return HitTestResult.createFromDOMInfo(ctx, parent1, range.startOffset); } else { - hitTarget = startContainer.parentNode; + return new UnknownHitTestResult(startContainer.parentNode); } } else if (startContainer.nodeType === startContainer.ELEMENT_NODE) { // startContainer is expected to be the token span @@ -891,26 +885,19 @@ export class MouseTargetFactory { const parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (parent2).className : null; if (parent2ClassName === ViewLine.CLASS_NAME) { - const p = ctx.getPositionFromDOMInfo(startContainer, (startContainer).textContent!.length); - return { - position: p, - hitTarget: null - }; + return HitTestResult.createFromDOMInfo(ctx, startContainer, (startContainer).textContent!.length); } else { - hitTarget = startContainer; + return new UnknownHitTestResult(startContainer); } } - return { - position: null, - hitTarget: hitTarget - }; + return new UnknownHitTestResult(); } /** * Most probably Gecko */ - private static _doHitTestWithCaretPositionFromPoint(ctx: HitTestContext, coords: ClientCoordinates): IHitTestResult { + private static _doHitTestWithCaretPositionFromPoint(ctx: HitTestContext, coords: ClientCoordinates): HitTestResult { const hitResult: { offsetNode: Node; offset: number; } = (document).caretPositionFromPoint(coords.clientX, coords.clientY); if (hitResult.offsetNode.nodeType === hitResult.offsetNode.TEXT_NODE) { @@ -921,16 +908,9 @@ export class MouseTargetFactory { const parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? (parent3).className : null; if (parent3ClassName === ViewLine.CLASS_NAME) { - const p = ctx.getPositionFromDOMInfo(hitResult.offsetNode.parentNode, hitResult.offset); - return { - position: p, - hitTarget: null - }; + return HitTestResult.createFromDOMInfo(ctx, hitResult.offsetNode.parentNode, hitResult.offset); } else { - return { - position: null, - hitTarget: hitResult.offsetNode.parentNode - }; + return new UnknownHitTestResult(hitResult.offsetNode.parentNode); } } @@ -946,26 +926,15 @@ export class MouseTargetFactory { // 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); - return { - position: p, - hitTarget: null - }; + return HitTestResult.createFromDOMInfo(ctx, tokenSpan, 0); } } 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 - }; + return HitTestResult.createFromDOMInfo(ctx, hitResult.offsetNode, 0); } } - return { - position: null, - hitTarget: hitResult.offsetNode - }; + return new UnknownHitTestResult(hitResult.offsetNode); } private static _snapToSoftTabBoundary(position: Position, viewModel: IViewModel): Position { @@ -978,22 +947,17 @@ export class MouseTargetFactory { return position; } - private static _doHitTest(ctx: HitTestContext, request: BareHitTestRequest): IHitTestResult { + private static _doHitTest(ctx: HitTestContext, request: BareHitTestRequest): HitTestResult { - let result: IHitTestResult; + let result: HitTestResult = new UnknownHitTestResult(); if (typeof document.caretRangeFromPoint === 'function') { result = this._doHitTestWithCaretRangeFromPoint(ctx, request); } else if ((document).caretPositionFromPoint) { result = this._doHitTestWithCaretPositionFromPoint(ctx, request.pos.toClientCoordinates()); - } else { - result = { - position: null, - hitTarget: null - }; } // Snap to the nearest soft tab boundary if atomic soft tabs are enabled. - if (result.position && ctx.stickyTabStops) { - result.position = this._snapToSoftTabBoundary(result.position, ctx.model); + if (result.type === HitTestResultType.Content && ctx.stickyTabStops) { + result = new ContentHitTestResult(this._snapToSoftTabBoundary(result.position, ctx.model), result.spanNode); } return result; } diff --git a/src/vs/editor/browser/core/markdownRenderer.ts b/src/vs/editor/browser/core/markdownRenderer.ts index f629f2664e..1db133a0e4 100644 --- a/src/vs/editor/browser/core/markdownRenderer.ts +++ b/src/vs/editor/browser/core/markdownRenderer.ts @@ -52,18 +52,18 @@ export class MarkdownRenderer { } render(markdown: IMarkdownString | undefined, options?: MarkdownRenderOptions, markedOptions?: MarkedOptions): IMarkdownRenderResult { - const disposeables = new DisposableStore(); + const disposables = new DisposableStore(); let element: HTMLElement; if (!markdown) { element = document.createElement('span'); } else { - element = renderMarkdown(markdown, { ...this._getRenderOptions(markdown, disposeables), ...options }, markedOptions); + element = renderMarkdown(markdown, { ...this._getRenderOptions(markdown, disposables), ...options }, markedOptions); } return { element, - dispose: () => disposeables.dispose() + dispose: () => disposables.dispose() }; } diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index a86d227ab9..eccc5beca8 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -622,13 +622,13 @@ export interface ICodeEditor extends editorCommon.IEditor { /** * Get value of the current model attached to this editor. - * @see `ITextModel.getValue` + * @see {@link ITextModel.getValue} */ getValue(options?: { preserveBOM: boolean; lineEnding: string; }): string; /** * Set the value of the current model attached to this editor. - * @see `ITextModel.setValue` + * @see {@link ITextModel.setValue} */ setValue(newValue: string): void; @@ -726,14 +726,14 @@ export interface ICodeEditor extends editorCommon.IEditor { /** * All decorations added through this call will get the ownerId of this editor. - * @see `ITextModel.deltaDecorations` + * @see {@link ITextModel.deltaDecorations} */ deltaDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[]; /** * @internal */ - setDecorations(decorationTypeKey: string, ranges: editorCommon.IDecorationOptions[]): void; + setDecorations(description: string, decorationTypeKey: string, ranges: editorCommon.IDecorationOptions[]): void; /** * @internal @@ -975,7 +975,7 @@ export interface IDiffEditor extends editorCommon.IEditor { readonly maxComputationTime: number; /** - * @see ICodeEditor.getDomNode + * @see {@link ICodeEditor.getDomNode} */ getDomNode(): HTMLElement; diff --git a/src/vs/editor/browser/editorExtensions.ts b/src/vs/editor/browser/editorExtensions.ts index 4046594cf6..432b9df4ba 100644 --- a/src/vs/editor/browser/editorExtensions.ts +++ b/src/vs/editor/browser/editorExtensions.ts @@ -61,14 +61,14 @@ export interface ICommandMenuOptions { export interface ICommandOptions { id: string; precondition: ContextKeyExpression | undefined; - kbOpts?: ICommandKeybindingsOptions; + kbOpts?: ICommandKeybindingsOptions | ICommandKeybindingsOptions[]; description?: ICommandHandlerDescription; menuOpts?: ICommandMenuOptions | ICommandMenuOptions[]; } export abstract class Command { public readonly id: string; public readonly precondition: ContextKeyExpression | undefined; - private readonly _kbOpts: ICommandKeybindingsOptions | undefined; + private readonly _kbOpts: ICommandKeybindingsOptions | ICommandKeybindingsOptions[] | undefined; private readonly _menuOpts: ICommandMenuOptions | ICommandMenuOptions[] | undefined; private readonly _description: ICommandHandlerDescription | undefined; @@ -89,37 +89,38 @@ export abstract class Command { } if (this._kbOpts) { - let kbWhen = this._kbOpts.kbExpr; - if (this.precondition) { - if (kbWhen) { - kbWhen = ContextKeyExpr.and(kbWhen, this.precondition); - } else { - kbWhen = this.precondition; + const kbOptsArr = Array.isArray(this._kbOpts) ? this._kbOpts : [this._kbOpts]; + for (const kbOpts of kbOptsArr) { + let kbWhen = kbOpts.kbExpr; + if (this.precondition) { + if (kbWhen) { + kbWhen = ContextKeyExpr.and(kbWhen, this.precondition); + } else { + kbWhen = this.precondition; + } } + + const desc = { + id: this.id, + weight: kbOpts.weight, + args: kbOpts.args, + when: kbWhen, + primary: kbOpts.primary, + secondary: kbOpts.secondary, + win: kbOpts.win, + linux: kbOpts.linux, + mac: kbOpts.mac, + }; + + KeybindingsRegistry.registerKeybindingRule(desc); } - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: this.id, - handler: (accessor, args) => this.runCommand(accessor, args), - weight: this._kbOpts.weight, - args: this._kbOpts.args, - when: kbWhen, - primary: this._kbOpts.primary, - secondary: this._kbOpts.secondary, - win: this._kbOpts.win, - linux: this._kbOpts.linux, - mac: this._kbOpts.mac, - description: this._description - }); - - } else { - - CommandsRegistry.registerCommand({ - id: this.id, - handler: (accessor, args) => this.runCommand(accessor, args), - description: this._description - }); } + + CommandsRegistry.registerCommand({ + id: this.id, + handler: (accessor, args) => this.runCommand(accessor, args), + description: this._description + }); } private _registerMenuItem(item: ICommandMenuOptions): void { @@ -348,14 +349,16 @@ export abstract class EditorAction extends EditorCommand { public abstract run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise; } +export type EditorActionImplementation = (accessor: ServicesAccessor, editor: ICodeEditor, args: any) => boolean | Promise; + export class MultiEditorAction extends EditorAction { - private readonly _implementations: [number, CommandImplementation][] = []; + private readonly _implementations: [number, EditorActionImplementation][] = []; /** * A higher priority gets to be looked at first */ - public addImplementation(priority: number, implementation: CommandImplementation): IDisposable { + public addImplementation(priority: number, implementation: EditorActionImplementation): IDisposable { this._implementations.push([priority, implementation]); this._implementations.sort((a, b) => b[0] - a[0]); return { @@ -372,7 +375,7 @@ export class MultiEditorAction extends EditorAction { public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise { for (const impl of this._implementations) { - const result = impl[1](accessor, args); + const result = impl[1](accessor, editor, args); if (result) { if (typeof result === 'boolean') { return; diff --git a/src/vs/editor/browser/services/abstractCodeEditorService.ts b/src/vs/editor/browser/services/abstractCodeEditorService.ts index 4eec3917e5..ae6c524e3f 100644 --- a/src/vs/editor/browser/services/abstractCodeEditorService.ts +++ b/src/vs/editor/browser/services/abstractCodeEditorService.ts @@ -92,7 +92,7 @@ export abstract class AbstractCodeEditorService extends Disposable implements IC return editorWithWidgetFocus; } - abstract registerDecorationType(key: string, options: IDecorationRenderOptions, parentTypeKey?: string, editor?: ICodeEditor): void; + abstract registerDecorationType(description: string, key: string, options: IDecorationRenderOptions, parentTypeKey?: string, editor?: ICodeEditor): void; abstract removeDecorationType(key: string): void; abstract resolveDecorationOptions(decorationTypeKey: string | undefined, writable: boolean): IModelDecorationOptions; abstract resolveDecorationCSSRules(decorationTypeKey: string): CSSRuleList | null; diff --git a/src/vs/editor/browser/services/codeEditorService.ts b/src/vs/editor/browser/services/codeEditorService.ts index 4dbc136f06..356f5b040e 100644 --- a/src/vs/editor/browser/services/codeEditorService.ts +++ b/src/vs/editor/browser/services/codeEditorService.ts @@ -7,7 +7,7 @@ import { Event } from 'vs/base/common/event'; import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IDecorationRenderOptions } from 'vs/editor/common/editorCommon'; import { IModelDecorationOptions, ITextModel } from 'vs/editor/common/model'; -import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; @@ -39,7 +39,7 @@ export interface ICodeEditorService { */ getFocusedCodeEditor(): ICodeEditor | null; - registerDecorationType(key: string, options: IDecorationRenderOptions, parentTypeKey?: string, editor?: ICodeEditor): void; + registerDecorationType(description: string, key: string, options: IDecorationRenderOptions, parentTypeKey?: string, editor?: ICodeEditor): void; removeDecorationType(key: string): void; resolveDecorationOptions(typeKey: string, writable: boolean): IModelDecorationOptions; resolveDecorationCSSRules(decorationTypeKey: string): CSSRuleList | null; @@ -52,5 +52,5 @@ export interface ICodeEditorService { getTransientModelProperties(model: ITextModel): [string, any][] | undefined; getActiveCodeEditor(): ICodeEditor | null; - openCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise; + openCodeEditor(input: ITextResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise; } diff --git a/src/vs/editor/browser/services/codeEditorServiceImpl.ts b/src/vs/editor/browser/services/codeEditorServiceImpl.ts index 4d0fdbab5d..7663e8a845 100644 --- a/src/vs/editor/browser/services/codeEditorServiceImpl.ts +++ b/src/vs/editor/browser/services/codeEditorServiceImpl.ts @@ -123,7 +123,7 @@ export abstract class CodeEditorServiceImpl extends AbstractCodeEditorService { this._editorStyleSheets.delete(editorId); } - public registerDecorationType(key: string, options: IDecorationRenderOptions, parentTypeKey?: string, editor?: ICodeEditor): void { + public registerDecorationType(description: string, key: string, options: IDecorationRenderOptions, parentTypeKey?: string, editor?: ICodeEditor): void { let provider = this._decorationOptionProviders.get(key); if (!provider) { const styleSheet = this._getOrCreateStyleSheet(editor); @@ -134,7 +134,7 @@ export abstract class CodeEditorServiceImpl extends AbstractCodeEditorService { options: options || Object.create(null) }; if (!parentTypeKey) { - provider = new DecorationTypeOptionsProvider(this._themeService, styleSheet, providerArgs); + provider = new DecorationTypeOptionsProvider(description, this._themeService, styleSheet, providerArgs); } else { provider = new DecorationSubTypeOptionsProvider(this._themeService, styleSheet, providerArgs); } @@ -240,6 +240,7 @@ export class DecorationTypeOptionsProvider implements IModelDecorationOptionsPro private readonly _styleSheet: GlobalStyleSheet | RefCountedStyleSheet; public refCount: number; + public description: string; public className: string | undefined; public inlineClassName: string | undefined; public inlineClassNameAffectsLetterSpacing: boolean | undefined; @@ -250,7 +251,9 @@ export class DecorationTypeOptionsProvider implements IModelDecorationOptionsPro public overviewRuler: IModelDecorationOverviewRulerOptions | undefined; public stickiness: TrackedRangeStickiness | undefined; - constructor(themeService: IThemeService, styleSheet: GlobalStyleSheet | RefCountedStyleSheet, providerArgs: ProviderArguments) { + constructor(description: string, themeService: IThemeService, styleSheet: GlobalStyleSheet | RefCountedStyleSheet, providerArgs: ProviderArguments) { + this.description = description; + this._styleSheet = styleSheet; this._styleSheet.ref(); this.refCount = 0; @@ -305,6 +308,7 @@ export class DecorationTypeOptionsProvider implements IModelDecorationOptionsPro return this; } return { + description: this.description, inlineClassName: this.inlineClassName, beforeContentClassName: this.beforeContentClassName, afterContentClassName: this.afterContentClassName, diff --git a/src/vs/editor/browser/services/markerDecorations.ts b/src/vs/editor/browser/services/markerDecorations.ts index 79bec003a5..cf5c8cd824 100644 --- a/src/vs/editor/browser/services/markerDecorations.ts +++ b/src/vs/editor/browser/services/markerDecorations.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDecorationService'; diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index cca448bb1a..f4f7d39342 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -81,7 +81,7 @@ class EditorOpener implements IOpener { await this._editorService.openCodeEditor( { - resource: target, + resource: target as URI, // {{SQL CARBON EDIT}} Cast to URI to fix strict compiler error options: { selection, context: options?.fromUserGesture ? EditorOpenContext.USER : EditorOpenContext.API, @@ -191,31 +191,41 @@ export class OpenerService implements IOpenerService { async resolveExternalUri(resource: URI, options?: ResolveExternalUriOptions): Promise { for (const resolver of this._resolvers) { - const result = await resolver.resolveExternalUri(resource, options); - if (result) { - if (!this._resolvedUriTargets.has(result.resolved)) { - this._resolvedUriTargets.set(result.resolved, resource); + try { + const result = await resolver.resolveExternalUri(resource, options); + if (result) { + if (!this._resolvedUriTargets.has(result.resolved)) { + this._resolvedUriTargets.set(result.resolved, resource); + } + return result; } - return result; + } catch { + // noop } } - return { resolved: resource, dispose: () => { } }; + throw new Error('Could not resolve external URI: ' + resource.toString()); } private async _doOpenExternal(resource: URI | string, options: OpenOptions | undefined): Promise { //todo@jrieken IExternalUriResolver should support `uri: URI | string` const uri = typeof resource === 'string' ? URI.parse(resource) : resource; - const { resolved } = await this.resolveExternalUri(uri, options); + let externalUri: URI; + + try { + externalUri = (await this.resolveExternalUri(uri, options)).resolved; + } catch { + externalUri = uri; + } let href: string; - if (typeof resource === 'string' && uri.toString() === resolved.toString()) { + if (typeof resource === 'string' && uri.toString() === externalUri.toString()) { // open the url-string AS IS href = resource; } else { // open URI using the toString(noEncode)+encodeURI-trick - href = encodeURI(resolved.toString(true)); + href = encodeURI(externalUri.toString(true)); } if (options?.allowContributedOpeners) { diff --git a/src/vs/editor/browser/viewParts/decorations/decorations.css b/src/vs/editor/browser/viewParts/decorations/decorations.css index 795df82686..e112cff33d 100644 --- a/src/vs/editor/browser/viewParts/decorations/decorations.css +++ b/src/vs/editor/browser/viewParts/decorations/decorations.css @@ -9,4 +9,4 @@ */ .monaco-editor .lines-content .cdr { position: absolute; -} \ No newline at end of file +} diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index 708cdd9e7e..16fe85d523 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -12,7 +12,7 @@ import { IStringBuilder } from 'vs/editor/common/core/stringBuilder'; import { IConfiguration } from 'vs/editor/common/editorCommon'; import { HorizontalRange, VisibleRanges } from 'vs/editor/common/view/renderingContext'; import { LineDecoration } from 'vs/editor/common/viewLayout/lineDecorations'; -import { CharacterMapping, ForeignElementType, RenderLineInput, renderViewLine, LineRange } from 'vs/editor/common/viewLayout/viewLineRenderer'; +import { CharacterMapping, ForeignElementType, RenderLineInput, renderViewLine, LineRange, DomPosition } from 'vs/editor/common/viewLayout/viewLineRenderer'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { InlineDecorationType } from 'vs/editor/common/viewModel/viewModel'; import { ColorScheme } from 'vs/platform/theme/common/theme'; @@ -430,12 +430,8 @@ class FastRenderedViewLine implements IRenderedViewLine { } private _getCharPosition(column: number): number { - const charOffset = this._characterMapping.getAbsoluteOffsets(); - if (charOffset.length === 0) { - // No characters on this line - return 0; - } - return Math.round(this._charWidth * charOffset[column - 1]); + const charOffset = this._characterMapping.getAbsoluteOffset(column); + return Math.round(this._charWidth * charOffset); } public getColumnOfNodeOffset(lineNumber: number, spanNode: HTMLElement, offset: number): number { @@ -447,8 +443,7 @@ class FastRenderedViewLine implements IRenderedViewLine { spanIndex++; } - const charOffset = this._characterMapping.partDataToCharOffset(spanIndex, spanNodeTextContentLength, offset); - return charOffset + 1; + return this._characterMapping.getColumn(new DomPosition(spanIndex, offset), spanNodeTextContentLength); } } @@ -606,18 +601,16 @@ class RenderedViewLine implements IRenderedViewLine { return this.getWidth(); } - const partData = this._characterMapping.charOffsetToPartData(column - 1); - const partIndex = CharacterMapping.getPartIndex(partData); - const charOffsetInPart = CharacterMapping.getCharIndex(partData); + const domPosition = this._characterMapping.getDomPosition(column); - const r = RangeUtil.readHorizontalRanges(this._getReadingTarget(domNode), partIndex, charOffsetInPart, partIndex, charOffsetInPart, context.clientRectDeltaLeft, context.endNode); + const r = RangeUtil.readHorizontalRanges(this._getReadingTarget(domNode), domPosition.partIndex, domPosition.charIndex, domPosition.partIndex, domPosition.charIndex, context.clientRectDeltaLeft, context.endNode); if (!r || r.length === 0) { return -1; } const result = r[0].left; if (this.input.isBasicASCII) { - const charOffset = this._characterMapping.getAbsoluteOffsets(); - const expectedResult = Math.round(this.input.spaceWidth * charOffset[column - 1]); + const charOffset = this._characterMapping.getAbsoluteOffset(column); + const expectedResult = Math.round(this.input.spaceWidth * charOffset); if (Math.abs(expectedResult - result) <= 1) { return expectedResult; } @@ -633,15 +626,10 @@ class RenderedViewLine implements IRenderedViewLine { return [new HorizontalRange(0, this.getWidth())]; } - const startPartData = this._characterMapping.charOffsetToPartData(startColumn - 1); - const startPartIndex = CharacterMapping.getPartIndex(startPartData); - const startCharOffsetInPart = CharacterMapping.getCharIndex(startPartData); + const startDomPosition = this._characterMapping.getDomPosition(startColumn); + const endDomPosition = this._characterMapping.getDomPosition(endColumn); - const endPartData = this._characterMapping.charOffsetToPartData(endColumn - 1); - const endPartIndex = CharacterMapping.getPartIndex(endPartData); - const endCharOffsetInPart = CharacterMapping.getCharIndex(endPartData); - - return RangeUtil.readHorizontalRanges(this._getReadingTarget(domNode), startPartIndex, startCharOffsetInPart, endPartIndex, endCharOffsetInPart, context.clientRectDeltaLeft, context.endNode); + return RangeUtil.readHorizontalRanges(this._getReadingTarget(domNode), startDomPosition.partIndex, startDomPosition.charIndex, endDomPosition.partIndex, endDomPosition.charIndex, context.clientRectDeltaLeft, context.endNode); } /** @@ -656,8 +644,7 @@ class RenderedViewLine implements IRenderedViewLine { spanIndex++; } - const charOffset = this._characterMapping.partDataToCharOffset(spanIndex, spanNodeTextContentLength, offset); - return charOffset + 1; + return this._characterMapping.getColumn(new DomPosition(spanIndex, offset), spanNodeTextContentLength); } } diff --git a/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.css b/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.css index ebd7e29087..fab9235b03 100644 --- a/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.css +++ b/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.css @@ -15,4 +15,4 @@ .monaco-editor .margin-view-overlays .cldr { position: absolute; height: 100%; -} \ No newline at end of file +} diff --git a/src/vs/editor/browser/viewParts/marginDecorations/marginDecorations.css b/src/vs/editor/browser/viewParts/marginDecorations/marginDecorations.css index 46c36429f2..acf3a32620 100644 --- a/src/vs/editor/browser/viewParts/marginDecorations/marginDecorations.css +++ b/src/vs/editor/browser/viewParts/marginDecorations/marginDecorations.css @@ -12,4 +12,4 @@ left: 0; width: 100%; height: 100%; -} \ No newline at end of file +} diff --git a/src/vs/editor/browser/viewParts/marginDecorations/marginDecorations.ts b/src/vs/editor/browser/viewParts/marginDecorations/marginDecorations.ts index 58f2e75fdb..a2524bbfae 100644 --- a/src/vs/editor/browser/viewParts/marginDecorations/marginDecorations.ts +++ b/src/vs/editor/browser/viewParts/marginDecorations/marginDecorations.ts @@ -93,4 +93,4 @@ export class MarginViewLineDecorationsOverlay extends DedupOverlay { } return this._renderResult[lineNumber - startLineNumber]; } -} \ No newline at end of file +} diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index 2447866691..86579a1e82 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -1120,7 +1120,7 @@ class InnerMinimap extends Disposable { } if (this._model.options.size !== 'proportional') { if (e.leftButton && this._lastRenderData) { - // pretend the click occured in the center of the slider + // pretend the click occurred in the center of the slider const position = dom.getDomNodePagePosition(this._slider.domNode); const initialPosY = position.top + position.height / 2; this._startSliderDragging(e.buttons, e.posx, initialPosY, e.posy, this._lastRenderData.renderedLayout); diff --git a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.css b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.css index fee16d85b1..070bdd604a 100644 --- a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.css +++ b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.css @@ -6,4 +6,4 @@ position: absolute; top: 0; left:0; -} \ No newline at end of file +} diff --git a/src/vs/editor/browser/viewParts/rulers/rulers.css b/src/vs/editor/browser/viewParts/rulers/rulers.css index 702f59dfa2..297e600635 100644 --- a/src/vs/editor/browser/viewParts/rulers/rulers.css +++ b/src/vs/editor/browser/viewParts/rulers/rulers.css @@ -6,4 +6,4 @@ .monaco-editor .view-ruler { position: absolute; top: 0; -} \ No newline at end of file +} diff --git a/src/vs/editor/browser/viewParts/scrollDecoration/scrollDecoration.css b/src/vs/editor/browser/viewParts/scrollDecoration/scrollDecoration.css index 26a5797e52..be81126536 100644 --- a/src/vs/editor/browser/viewParts/scrollDecoration/scrollDecoration.css +++ b/src/vs/editor/browser/viewParts/scrollDecoration/scrollDecoration.css @@ -8,4 +8,4 @@ top: 0; left: 0; height: 6px; -} \ No newline at end of file +} diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index e36d30ed83..ba95de7a04 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -217,8 +217,8 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE private readonly _id: number; private readonly _configuration: editorCommon.IConfiguration; - protected readonly _contributions: { [key: string]: editorCommon.IEditorContribution; }; - protected readonly _actions: { [key: string]: editorCommon.IEditorAction; }; + protected _contributions: { [key: string]: editorCommon.IEditorContribution; }; + protected _actions: { [key: string]: editorCommon.IEditorAction; }; // --- Members logically associated to a model protected _modelData: ModelData | null; @@ -226,14 +226,14 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE protected readonly _instantiationService: IInstantiationService; protected readonly _contextKeyService: IContextKeyService; private readonly _notificationService: INotificationService; - private readonly _codeEditorService: ICodeEditorService; + protected readonly _codeEditorService: ICodeEditorService; private readonly _commandService: ICommandService; private readonly _themeService: IThemeService; private readonly _focusTracker: CodeEditorWidgetFocusTracker; - private readonly _contentWidgets: { [key: string]: IContentWidgetData; }; - private readonly _overlayWidgets: { [key: string]: IOverlayWidgetData; }; + private _contentWidgets: { [key: string]: IContentWidgetData; }; + private _overlayWidgets: { [key: string]: IOverlayWidgetData; }; /** * map from "parent" decoration type to live decoration ids. @@ -307,6 +307,10 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE contributions = EditorExtensionsRegistry.getEditorContributions(); } for (const desc of contributions) { + if (this._contributions[desc.id]) { + onUnexpectedError(new Error(`Cannot have two contributions with the same id ${desc.id}`)); + continue; + } try { const contribution = this._instantiationService.createInstance(desc.ctor, this); this._contributions[desc.id] = contribution; @@ -316,6 +320,10 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE } EditorExtensionsRegistry.getEditorActions().forEach((action) => { + if (this._actions[action.id]) { + onUnexpectedError(new Error(`Cannot have two actions with the same id ${action.id}`)); + return; + } const internalAction = new InternalEditorAction( action.id, action.label, @@ -356,6 +364,10 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE const contributionId = keys[i]; this._contributions[contributionId].dispose(); } + this._contributions = {}; + this._actions = {}; + this._contentWidgets = {}; + this._overlayWidgets = {}; this._removeDecorationTypes(); this._postDetachModelCleanup(this._detachModel()); @@ -1036,6 +1048,10 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return; } + this._triggerCommand(handlerId, payload); + } + + protected _triggerCommand(handlerId: string, payload: any): void { this._commandService.executeCommand(handlerId, payload); } @@ -1205,7 +1221,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return this._modelData.model.deltaDecorations(oldDecorations, newDecorations, this._id); } - public setDecorations(decorationTypeKey: string, decorationOptions: editorCommon.IDecorationOptions[]): void { + public setDecorations(description: string, decorationTypeKey: string, decorationOptions: editorCommon.IDecorationOptions[]): void { const newDecorationsSubTypes: { [key: string]: boolean } = {}; const oldDecorationsSubTypes = this._decorationTypeSubtypes[decorationTypeKey] || {}; @@ -1224,7 +1240,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE typeKey = decorationTypeKey + '-' + subType; if (!oldDecorationsSubTypes[subType] && !newDecorationsSubTypes[subType]) { // decoration type did not exist before, register new one - this._registerDecorationType(typeKey, decorationOption.renderOptions, decorationTypeKey); + this._registerDecorationType(description, typeKey, decorationOption.renderOptions, decorationTypeKey); } newDecorationsSubTypes[subType] = true; } @@ -1685,8 +1701,8 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return model; } - private _registerDecorationType(key: string, options: editorCommon.IDecorationRenderOptions, parentTypeKey?: string): void { - this._codeEditorService.registerDecorationType(key, options, parentTypeKey, this); + private _registerDecorationType(description: string, key: string, options: editorCommon.IDecorationRenderOptions, parentTypeKey?: string): void { + this._codeEditorService.registerDecorationType(description, key, options, parentTypeKey, this); } private _removeDecorationType(key: string): void { @@ -1849,7 +1865,7 @@ export class EditorModeContext extends Disposable { private readonly _hasMultipleDocumentFormattingProvider: IContextKey; private readonly _hasMultipleDocumentSelectionFormattingProvider: IContextKey; private readonly _hasSignatureHelpProvider: IContextKey; - private readonly _hasInlineHintsProvider: IContextKey; + private readonly _hasInlayHintsProvider: IContextKey; private readonly _isInWalkThrough: IContextKey; constructor( @@ -1872,7 +1888,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._hasInlayHintsProvider = EditorContextKeys.hasInlayHintsProvider.bindTo(_contextKeyService); this._hasDocumentFormattingProvider = EditorContextKeys.hasDocumentFormattingProvider.bindTo(_contextKeyService); this._hasDocumentSelectionFormattingProvider = EditorContextKeys.hasDocumentSelectionFormattingProvider.bindTo(_contextKeyService); this._hasMultipleDocumentFormattingProvider = EditorContextKeys.hasMultipleDocumentFormattingProvider.bindTo(_contextKeyService); @@ -1901,7 +1917,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)); + this._register(modes.InlayHintsProviderRegistry.onDidChange(update)); update(); } @@ -1953,7 +1969,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._hasInlayHintsProvider.set(modes.InlayHintsProviderRegistry.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 c470f382f8..bd7367403c 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -1792,27 +1792,33 @@ function createDecoration(startLineNumber: number, startColumn: number, endLineN const DECORATIONS = { charDelete: ModelDecorationOptions.register({ + description: 'diff-editor-char-delete', className: 'char-delete' }), charDeleteWholeLine: ModelDecorationOptions.register({ + description: 'diff-editor-char-delete-whole-line', className: 'char-delete', isWholeLine: true }), charInsert: ModelDecorationOptions.register({ + description: 'diff-editor-char-insert', className: 'char-insert' }), charInsertWholeLine: ModelDecorationOptions.register({ + description: 'diff-editor-char-insert-whole-line', className: 'char-insert', isWholeLine: true }), lineInsert: ModelDecorationOptions.register({ + description: 'diff-editor-line-insert', className: 'line-insert', marginClassName: 'line-insert', isWholeLine: true }), lineInsertWithSign: ModelDecorationOptions.register({ + description: 'diff-editor-line-insert-with-sign', className: 'line-insert', linesDecorationsClassName: 'insert-sign ' + ThemeIcon.asClassName(diffInsertIcon), marginClassName: 'line-insert', @@ -1820,11 +1826,13 @@ const DECORATIONS = { }), lineDelete: ModelDecorationOptions.register({ + description: 'diff-editor-line-delete', className: 'line-delete', marginClassName: 'line-delete', isWholeLine: true }), lineDeleteWithSign: ModelDecorationOptions.register({ + description: 'diff-editor-line-delete-with-sign', className: 'line-delete', linesDecorationsClassName: 'delete-sign ' + ThemeIcon.asClassName(diffRemoveIcon), marginClassName: 'line-delete', @@ -1832,6 +1840,7 @@ const DECORATIONS = { }), lineDeleteMargin: ModelDecorationOptions.register({ + description: 'diff-editor-line-delete-margin', marginClassName: 'line-delete', }) @@ -2521,8 +2530,7 @@ class InlineViewZonesComputer extends ViewZonesComputer { marginDomNode.appendChild(marginElement); } - const absoluteOffsets = output.characterMapping.getAbsoluteOffsets(); - return absoluteOffsets.length > 0 ? absoluteOffsets[absoluteOffsets.length - 1] : 0; + return output.characterMapping.getAbsoluteOffset(output.characterMapping.length); } } diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index b15d1ea776..56230f8b9c 100644 --- a/src/vs/editor/browser/widget/diffReview.ts +++ b/src/vs/editor/browser/widget/diffReview.ts @@ -22,7 +22,6 @@ import { LineTokens } from 'vs/editor/common/core/lineTokens'; import { Position } from 'vs/editor/common/core/position'; import { ILineChange, ScrollType } from 'vs/editor/common/editorCommon'; import { ITextModel, TextModelResolvedOptions } from 'vs/editor/common/model'; -import { ColorId, FontStyle, MetadataConsts } from 'vs/editor/common/modes'; import { editorLineNumbers } from 'vs/editor/common/view/editorColorRegistry'; import { RenderLineInput, renderViewLine2 as renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer'; import { ViewLineRenderingData } from 'vs/editor/common/viewModel/viewModel'; @@ -777,19 +776,7 @@ export class DiffReview extends Disposable { private static _renderLine(model: ITextModel, options: IComputedEditorOptions, tabSize: number, lineNumber: number): string { const lineContent = model.getLineContent(lineNumber); const fontInfo = options.get(EditorOption.fontInfo); - - const defaultMetadata = ( - (FontStyle.None << MetadataConsts.FONT_STYLE_OFFSET) - | (ColorId.DefaultForeground << MetadataConsts.FOREGROUND_OFFSET) - | (ColorId.DefaultBackground << MetadataConsts.BACKGROUND_OFFSET) - ) >>> 0; - - const tokens = new Uint32Array(2); - tokens[0] = lineContent.length; - tokens[1] = defaultMetadata; - - const lineTokens = new LineTokens(tokens, lineContent); - + const lineTokens = LineTokens.createEmpty(lineContent); const isBasicASCII = ViewLineRenderingData.isBasicASCII(lineContent, model.mightContainNonBasicASCII()); const containsRTL = ViewLineRenderingData.containsRTL(lineContent, isBasicASCII, model.mightContainRTL()); const r = renderViewLine(new RenderLineInput( diff --git a/src/vs/editor/common/commands/shiftCommand.ts b/src/vs/editor/common/commands/shiftCommand.ts index 95e4643a9a..8760d2617e 100644 --- a/src/vs/editor/common/commands/shiftCommand.ts +++ b/src/vs/editor/common/commands/shiftCommand.ts @@ -24,6 +24,9 @@ export interface IShiftCommandOpts { const repeatCache: { [str: string]: string[]; } = Object.create(null); export function cachedStringRepeat(str: string, count: number): string { + if (count <= 0) { + return ''; + } if (!repeatCache[str]) { repeatCache[str] = ['', str]; } diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index de73223291..979943a753 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -204,6 +204,7 @@ function migrateOptions(options: IEditorOptions): void { mapping['method'] = 'showMethods'; mapping['function'] = 'showFunctions'; mapping['constructor'] = 'showConstructors'; + mapping['deprecated'] = 'showDeprecated'; mapping['field'] = 'showFields'; mapping['variable'] = 'showVariables'; mapping['class'] = 'showClasses'; diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index b9e0c14138..a7e1389583 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -382,8 +382,9 @@ export interface IEditorOptions { * Suggest options. */ suggest?: ISuggestOptions; + inlineSuggest?: IInlineSuggestOptions; /** - * Smart select opptions; + * Smart select options. */ smartSelect?: ISmartSelectOptions; /** @@ -637,7 +638,11 @@ export interface IEditorOptions { /** * Control the behavior and rendering of the inline hints. */ - inlineHints?: IEditorInlineHintsOptions; + inlayHints?: IEditorInlayHintsOptions; + /** + * Control if the editor should use shadow DOM. + */ + useShadowDOM?: boolean; } /** @@ -2401,12 +2406,12 @@ class EditorLightbulb extends BaseEditorOption>; +export type EditorInlayHintsOptions = Readonly>; -class EditorInlineHints extends BaseEditorOption { +class EditorInlayHints extends BaseEditorOption { constructor() { - const defaults: EditorInlineHintsOptions = { enabled: true, fontSize: 0, fontFamily: EDITOR_FONT_DEFAULTS.fontFamily }; + const defaults: EditorInlayHintsOptions = { enabled: true, fontSize: 0, fontFamily: EDITOR_FONT_DEFAULTS.fontFamily }; super( - EditorOption.inlineHints, 'inlineHints', defaults, + EditorOption.inlayHints, 'inlayHints', defaults, { - 'editor.inlineHints.enabled': { + 'editor.inlayHints.enabled': { type: 'boolean', default: defaults.enabled, - description: nls.localize('inlineHints.enable', "Enables the inline hints in the editor.") + description: nls.localize('inlayHints.enable', "Enables the inlay hints in the editor.") }, - 'editor.inlineHints.fontSize': { + 'editor.inlayHints.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.") + description: nls.localize('inlayHints.fontSize', "Controls font size of inlay hints in the editor. When set to `0`, the 90% of `#editor.fontSize#` is used.") }, - 'editor.inlineHints.fontFamily': { + 'editor.inlayHints.fontFamily': { type: 'string', default: defaults.fontFamily, - description: nls.localize('inlineHints.fontFamily', "Controls font family of inline hints in the editor.") + description: nls.localize('inlayHints.fontFamily', "Controls font family of inlay hints in the editor.") }, } ); } - public validate(_input: any): EditorInlineHintsOptions { + public validate(_input: any): EditorInlayHintsOptions { if (!_input || typeof _input !== 'object') { return this.defaultValue; } - const input = _input as IEditorInlineHintsOptions; + const input = _input as IEditorInlayHintsOptions; return { enabled: boolean(input.enabled, this.defaultValue.enabled), fontSize: EditorIntOption.clampedInt(input.fontSize, this.defaultValue.fontSize, 0, 100), @@ -3141,6 +3146,51 @@ class EditorScrollbar extends BaseEditorOption>; + +/** + * Configuration options for inline suggestions + */ +class InlineEditorSuggest extends BaseEditorOption { + constructor() { + const defaults: InternalInlineSuggestOptions = { + enabled: false + }; + + super( + EditorOption.inlineSuggest, 'inlineSuggest', defaults, + { + 'editor.inlineSuggest.enabled': { + type: 'boolean', + default: defaults.enabled, + description: nls.localize('inlineSuggest.enabled', "Controls whether to automatically show inline suggestions in the editor.") + }, + } + ); + } + + public validate(_input: any): InternalInlineSuggestOptions { + if (!_input || typeof _input !== 'object') { + return this.defaultValue; + } + const input = _input as IInlineSuggestOptions; + return { + enabled: boolean(input.enabled, this.defaultValue.enabled), + }; + } +} + +//#endregion + //#region suggest /** @@ -3175,6 +3225,10 @@ export interface ISuggestOptions { * Enable or disable the suggest status bar. */ showStatusBar?: boolean; + /** + * Enable or disable the rendering of the suggestion preview. + */ + preview?: boolean; /** * Show details inline with the label. Defaults to true. */ @@ -3191,6 +3245,10 @@ export interface ISuggestOptions { * Show constructor-suggestions. */ showConstructors?: boolean; + /** + * Show deprecated-suggestions. + */ + showDeprecated?: boolean; /** * Show field-suggestions. */ @@ -3302,10 +3360,12 @@ class EditorSuggest extends BaseEditorOption(); + result.push(-1); + let pos = 0; + let i = 0; + while (i < endOffset) { + const codePoint = strings.getNextCodePoint(lineContent, endOffset, i); + i += (codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1); + + result.push(pos); + if (codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN) { + result.push(pos); + } + + if (codePoint === CharCode.Tab) { + pos = CursorColumns.nextRenderTabStop(pos, tabSize); + } else { + let graphemeBreakType = strings.getGraphemeBreakType(codePoint); + while (i < endOffset) { + const nextCodePoint = strings.getNextCodePoint(lineContent, endOffset, i); + const nextGraphemeBreakType = strings.getGraphemeBreakType(nextCodePoint); + if (strings.breakBetweenGraphemeBreakType(graphemeBreakType, nextGraphemeBreakType)) { + break; + } + i += (nextCodePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1); + + result.push(pos); + if (codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN) { + result.push(pos); + } + + graphemeBreakType = nextGraphemeBreakType; + } + if (strings.isFullWidthCharacter(codePoint) || strings.isEmojiImprecise(codePoint)) { + pos = pos + 2; + } else { + pos = pos + 1; + } + } + } + result.push(pos); + return result; + } + public static toStatusbarColumn(lineContent: string, column: number, tabSize: number): number { const lineContentLength = lineContent.length; const endOffset = column - 1 < lineContentLength ? column - 1 : lineContentLength; @@ -562,14 +619,14 @@ export class CursorColumns { * ATTENTION: This works with 0-based columns (as opposed to the regular 1-based columns) */ public static prevRenderTabStop(column: number, tabSize: number): number { - return column - 1 - (column - 1) % tabSize; + return Math.max(0, column - 1 - (column - 1) % tabSize); } /** * ATTENTION: This works with 0-based columns (as opposed to the regular 1-based columns) */ public static prevIndentTabStop(column: number, indentSize: number): number { - return column - 1 - (column - 1) % indentSize; + return Math.max(0, column - 1 - (column - 1) % indentSize); } } diff --git a/src/vs/editor/common/controller/cursorDeleteOperations.ts b/src/vs/editor/common/controller/cursorDeleteOperations.ts index 7870f97054..892afd6958 100644 --- a/src/vs/editor/common/controller/cursorDeleteOperations.ts +++ b/src/vs/editor/common/controller/cursorDeleteOperations.ts @@ -12,6 +12,7 @@ import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { ICommand } from 'vs/editor/common/editorCommon'; import { StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration'; +import { Position } from 'vs/editor/common/core/position'; export class DeleteOperations { @@ -25,7 +26,7 @@ export class DeleteOperations { if (deleteSelection.isEmpty()) { let position = selection.getPosition(); - let rightOfPosition = MoveOperations.right(config, model, position.lineNumber, position.column); + let rightOfPosition = MoveOperations.right(config, model, position); deleteSelection = new Range( rightOfPosition.lineNumber, rightOfPosition.column, @@ -141,63 +142,72 @@ export class DeleteOperations { } public static deleteLeft(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], autoClosedCharacters: Range[]): [boolean, Array] { - if (this.isAutoClosingPairDelete(config.autoClosingDelete, config.autoClosingBrackets, config.autoClosingQuotes, config.autoClosingPairs.autoClosingPairsOpenByEnd, model, selections, autoClosedCharacters)) { return this._runAutoClosingPairDelete(config, model, selections); } - let commands: Array = []; + const commands: Array = []; let shouldPushStackElementBefore = (prevEditOperationType !== EditOperationType.DeletingLeft); for (let i = 0, len = selections.length; i < len; i++) { - const selection = selections[i]; + let deleteRange = DeleteOperations.getDeleteRange(selections[i], model, config); - let deleteSelection: Range = selection; - - if (deleteSelection.isEmpty()) { - let position = selection.getPosition(); - - if (config.useTabStops && position.column > 1) { - let lineContent = model.getLineContent(position.lineNumber); - - let firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent); - let lastIndentationColumn = ( - firstNonWhitespaceIndex === -1 - ? /* entire string is whitespace */lineContent.length + 1 - : firstNonWhitespaceIndex + 1 - ); - - if (position.column <= lastIndentationColumn) { - let fromVisibleColumn = CursorColumns.visibleColumnFromColumn2(config, model, position); - let toVisibleColumn = CursorColumns.prevIndentTabStop(fromVisibleColumn, config.indentSize); - let toColumn = CursorColumns.columnFromVisibleColumn2(config, model, position.lineNumber, toVisibleColumn); - deleteSelection = new Range(position.lineNumber, toColumn, position.lineNumber, position.column); - } else { - deleteSelection = new Range(position.lineNumber, position.column - 1, position.lineNumber, position.column); - } - } else { - let leftOfPosition = MoveOperations.left(config, model, position.lineNumber, position.column); - deleteSelection = new Range( - leftOfPosition.lineNumber, - leftOfPosition.column, - position.lineNumber, - position.column - ); - } - } - - if (deleteSelection.isEmpty()) { - // Probably at beginning of file => ignore + // Ignore empty delete ranges, as they have no effect + // They happen if the cursor is at the beginning of the file. + if (deleteRange.isEmpty()) { commands[i] = null; continue; } - if (deleteSelection.startLineNumber !== deleteSelection.endLineNumber) { + if (deleteRange.startLineNumber !== deleteRange.endLineNumber) { shouldPushStackElementBefore = true; } - commands[i] = new ReplaceCommand(deleteSelection, ''); + commands[i] = new ReplaceCommand(deleteRange, ''); } return [shouldPushStackElementBefore, commands]; + + } + + private static getDeleteRange(selection: Selection, model: ICursorSimpleModel, config: CursorConfiguration,): Range { + if (!selection.isEmpty()) { + return selection; + } + + const position = selection.getPosition(); + + // Unintend when using tab stops and cursor is within indentation + if (config.useTabStops && position.column > 1) { + const lineContent = model.getLineContent(position.lineNumber); + + const firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent); + const lastIndentationColumn = ( + firstNonWhitespaceIndex === -1 + ? /* entire string is whitespace */ lineContent.length + 1 + : firstNonWhitespaceIndex + 1 + ); + + if (position.column <= lastIndentationColumn) { + const fromVisibleColumn = CursorColumns.visibleColumnFromColumn2(config, model, position); + const toVisibleColumn = CursorColumns.prevIndentTabStop(fromVisibleColumn, config.indentSize); + const toColumn = CursorColumns.columnFromVisibleColumn2(config, model, position.lineNumber, toVisibleColumn); + return new Range(position.lineNumber, toColumn, position.lineNumber, position.column); + } + } + + return Range.fromPositions(DeleteOperations.getPositionAfterDeleteLeft(position, model), position); + } + + private static getPositionAfterDeleteLeft(position: Position, model: ICursorSimpleModel): Position { + if (position.column > 1) { + // Convert 1-based columns to 0-based offsets and back. + const idx = strings.getLeftDeleteOffset(position.column - 1, model.getLineContent(position.lineNumber)); + return position.with(undefined, idx + 1); + } else if (position.lineNumber > 1) { + const newLine = position.lineNumber - 1; + return new Position(newLine, model.getLineMaxColumn(newLine)); + } else { + return position; + } } public static cut(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): EditOperationResult { diff --git a/src/vs/editor/common/controller/cursorMoveCommands.ts b/src/vs/editor/common/controller/cursorMoveCommands.ts index 750f1d90f6..841632b8d6 100644 --- a/src/vs/editor/common/controller/cursorMoveCommands.ts +++ b/src/vs/editor/common/controller/cursorMoveCommands.ts @@ -419,29 +419,11 @@ export class CursorMoveCommands { } private static _moveLeft(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean, noOfColumns: number): PartialCursorState[] { - const hasMultipleCursors = (cursors.length > 1); - let result: PartialCursorState[] = []; - for (let i = 0, len = cursors.length; i < len; i++) { - const cursor = cursors[i]; - const skipWrappingPointStop = hasMultipleCursors || !cursor.viewState.hasSelection(); - let newViewState = MoveOperations.moveLeft(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode, noOfColumns); - - 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) { - // stayed on the same model line => pass wrapping point where 2 view positions map to a single model position - newViewState = MoveOperations.moveLeft(viewModel.cursorConfig, viewModel, newViewState, inSelectionMode, 1); - } - } - - result[i] = CursorState.fromViewState(newViewState); - } - return result; + return cursors.map(cursor => + CursorState.fromViewState( + MoveOperations.moveLeft(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode, noOfColumns) + ) + ); } private static _moveHalfLineLeft(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { @@ -456,29 +438,11 @@ export class CursorMoveCommands { } private static _moveRight(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean, noOfColumns: number): PartialCursorState[] { - const hasMultipleCursors = (cursors.length > 1); - let result: PartialCursorState[] = []; - for (let i = 0, len = cursors.length; i < len; i++) { - const cursor = cursors[i]; - const skipWrappingPointStop = hasMultipleCursors || !cursor.viewState.hasSelection(); - let newViewState = MoveOperations.moveRight(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode, noOfColumns); - - 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) { - // stayed on the same model line => pass wrapping point where 2 view positions map to a single model position - newViewState = MoveOperations.moveRight(viewModel.cursorConfig, viewModel, newViewState, inSelectionMode, 1); - } - } - - result[i] = CursorState.fromViewState(newViewState); - } - return result; + return cursors.map(cursor => + CursorState.fromViewState( + MoveOperations.moveRight(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode, noOfColumns) + ) + ); } private static _moveHalfLineRight(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { diff --git a/src/vs/editor/common/controller/cursorMoveOperations.ts b/src/vs/editor/common/controller/cursorMoveOperations.ts index e60c892b52..160db2697e 100644 --- a/src/vs/editor/common/controller/cursorMoveOperations.ts +++ b/src/vs/editor/common/controller/cursorMoveOperations.ts @@ -9,6 +9,7 @@ import { Range } from 'vs/editor/common/core/range'; import * as strings from 'vs/base/common/strings'; import { Constants } from 'vs/base/common/uint'; import { AtomicTabMoveOperations, Direction } from 'vs/editor/common/controller/cursorAtomicMoveOperations'; +import { PositionNormalizationAffinity } from 'vs/editor/common/model'; export class CursorPosition { _cursorPositionBrand: void; @@ -25,51 +26,86 @@ export class CursorPosition { } export class MoveOperations { - - public static leftPosition(model: ICursorSimpleModel, lineNumber: number, column: number): Position { - if (column > model.getLineMinColumn(lineNumber)) { - column = column - strings.prevCharLength(model.getLineContent(lineNumber), column - 1); - } else if (lineNumber > 1) { - lineNumber = lineNumber - 1; - column = model.getLineMaxColumn(lineNumber); + public static leftPosition(model: ICursorSimpleModel, position: Position): Position { + if (position.column > model.getLineMinColumn(position.lineNumber)) { + return position.delta(undefined, -strings.prevCharLength(model.getLineContent(position.lineNumber), position.column - 1)); + } else if (position.lineNumber > 1) { + const newLineNumber = position.lineNumber - 1; + return new Position(newLineNumber, model.getLineMaxColumn(newLineNumber)); + } else { + return position; } - return new Position(lineNumber, column); } - 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 - 1, tabSize, Direction.Left); - if (newPosition === -1 || newPosition + 1 < minColumn) { - return this.leftPosition(model, lineNumber, column); + private static leftPositionAtomicSoftTabs(model: ICursorSimpleModel, position: Position, tabSize: number): Position { + if (position.column <= model.getLineIndentColumn(position.lineNumber)) { + const minColumn = model.getLineMinColumn(position.lineNumber); + const lineContent = model.getLineContent(position.lineNumber); + const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, position.column - 1, tabSize, Direction.Left); + if (newPosition !== -1 && newPosition + 1 >= minColumn) { + return new Position(position.lineNumber, newPosition + 1); + } } - return new Position(lineNumber, newPosition + 1); + return this.leftPosition(model, position); } - public static left(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number): CursorPosition { + private static left(config: CursorConfiguration, model: ICursorSimpleModel, position: Position): CursorPosition { const pos = config.stickyTabStops - ? MoveOperations.leftPositionAtomicSoftTabs(model, lineNumber, column, config.tabSize) - : MoveOperations.leftPosition(model, lineNumber, column); + ? MoveOperations.leftPositionAtomicSoftTabs(model, position, config.tabSize) + : MoveOperations.leftPosition(model, position); return new CursorPosition(pos.lineNumber, pos.column, 0); } + /** + * @param noOfColumns Must be either `1` + * or `Math.round(viewModel.getLineContent(viewLineNumber).length / 2)` (for half lines). + */ public static moveLeft(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean, noOfColumns: number): SingleCursorState { let lineNumber: number, column: number; if (cursor.hasSelection() && !inSelectionMode) { - // If we are in selection mode, move left without selection cancels selection and puts cursor at the beginning of the selection + // If the user has a selection and does not want to extend it, + // put the cursor at the beginning of the selection. lineNumber = cursor.selection.startLineNumber; column = cursor.selection.startColumn; } else { - let r = MoveOperations.left(config, model, cursor.position.lineNumber, cursor.position.column - (noOfColumns - 1)); - lineNumber = r.lineNumber; - column = r.column; + // This has no effect if noOfColumns === 1. + // It is ok to do so in the half-line scenario. + const pos = cursor.position.delta(undefined, -(noOfColumns - 1)); + // We clip the position before normalization, as normalization is not defined + // for possibly negative columns. + const normalizedPos = model.normalizePosition(MoveOperations.clipPositionColumn(pos, model), PositionNormalizationAffinity.Left); + const p = MoveOperations.left(config, model, normalizedPos); + + lineNumber = p.lineNumber; + column = p.column; } return cursor.move(inSelectionMode, lineNumber, column, 0); } + /** + * Adjusts the column so that it is within min/max of the line. + */ + private static clipPositionColumn(position: Position, model: ICursorSimpleModel): Position { + return new Position( + position.lineNumber, + MoveOperations.clipRange(position.column, model.getLineMinColumn(position.lineNumber), + model.getLineMaxColumn(position.lineNumber)) + ); + } + + private static clipRange(value: number, min: number, max: number): number { + if (value < min) { + return min; + } + if (value > max) { + return max; + } + return value; + } + public static rightPosition(model: ICursorSimpleModel, lineNumber: number, column: number): Position { if (column < model.getLineMaxColumn(lineNumber)) { column = column + strings.nextCharLength(model.getLineContent(lineNumber), column - 1); @@ -81,18 +117,20 @@ export class MoveOperations { } public static rightPositionAtomicSoftTabs(model: ICursorSimpleModel, lineNumber: number, column: number, tabSize: number, indentSize: number): Position { - const lineContent = model.getLineContent(lineNumber); - const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, column - 1, tabSize, Direction.Right); - if (newPosition === -1) { - return this.rightPosition(model, lineNumber, column); + if (column < model.getLineIndentColumn(lineNumber)) { + const lineContent = model.getLineContent(lineNumber); + const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, column - 1, tabSize, Direction.Right); + if (newPosition !== -1) { + return new Position(lineNumber, newPosition + 1); + } } - return new Position(lineNumber, newPosition + 1); + return this.rightPosition(model, lineNumber, column); } - public static right(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number): CursorPosition { + public static right(config: CursorConfiguration, model: ICursorSimpleModel, position: Position): CursorPosition { const pos = config.stickyTabStops - ? MoveOperations.rightPositionAtomicSoftTabs(model, lineNumber, column, config.tabSize, config.indentSize) - : MoveOperations.rightPosition(model, lineNumber, column); + ? MoveOperations.rightPositionAtomicSoftTabs(model, position.lineNumber, position.column, config.tabSize, config.indentSize) + : MoveOperations.rightPosition(model, position.lineNumber, position.column); return new CursorPosition(pos.lineNumber, pos.column, 0); } @@ -105,7 +143,9 @@ export class MoveOperations { lineNumber = cursor.selection.endLineNumber; column = cursor.selection.endColumn; } else { - let r = MoveOperations.right(config, model, cursor.position.lineNumber, cursor.position.column + (noOfColumns - 1)); + const pos = cursor.position.delta(undefined, noOfColumns - 1); + const normalizedPos = model.normalizePosition(MoveOperations.clipPositionColumn(pos, model), PositionNormalizationAffinity.Right); + const r = MoveOperations.right(config, model, normalizedPos); lineNumber = r.lineNumber; column = r.column; } diff --git a/src/vs/editor/common/controller/cursorTypeOperations.ts b/src/vs/editor/common/controller/cursorTypeOperations.ts index 35ddac8811..f60d2f5d58 100644 --- a/src/vs/editor/common/controller/cursorTypeOperations.ts +++ b/src/vs/editor/common/controller/cursorTypeOperations.ts @@ -260,8 +260,8 @@ export class TypeOperations { public static compositionType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number): EditOperationResult { const commands = selections.map(selection => this._compositionType(model, selection, text, replacePrevCharCnt, replaceNextCharCnt, positionDelta)); - return new EditOperationResult(EditOperationType.Typing, commands, { - shouldPushStackElementBefore: (prevEditOperationType !== EditOperationType.Typing), + return new EditOperationResult(EditOperationType.TypingOther, commands, { + shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, EditOperationType.TypingOther), shouldPushStackElementAfter: false }); } @@ -484,8 +484,8 @@ export class TypeOperations { const typeSelection = new Range(position.lineNumber, position.column, position.lineNumber, position.column + 1); commands[i] = new ReplaceCommand(typeSelection, ch); } - return new EditOperationResult(EditOperationType.Typing, commands, { - shouldPushStackElementBefore: (prevEditOperationType !== EditOperationType.Typing), + return new EditOperationResult(EditOperationType.TypingOther, commands, { + shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, EditOperationType.TypingOther), shouldPushStackElementAfter: false }); } @@ -636,7 +636,7 @@ export class TypeOperations { const selection = selections[i]; commands[i] = new TypeWithAutoClosingCommand(selection, ch, insertOpenCharacter, autoClosingPairClose); } - return new EditOperationResult(EditOperationType.Typing, commands, { + return new EditOperationResult(EditOperationType.TypingOther, commands, { shouldPushStackElementBefore: true, shouldPushStackElementAfter: false }); @@ -762,7 +762,7 @@ export class TypeOperations { let typeSelection = new Range(position.lineNumber, 1, position.lineNumber, position.column); const command = new ReplaceCommand(typeSelection, typeText); - return new EditOperationResult(EditOperationType.Typing, [command], { + return new EditOperationResult(getTypingOperation(typeText, prevEditOperationType), [command], { shouldPushStackElementBefore: false, shouldPushStackElementAfter: true }); @@ -803,7 +803,7 @@ export class TypeOperations { if (this._isAutoClosingOvertype(config, model, selections, autoClosedCharacters, ch)) { // Unfortunately, the close character is at this point "doubled", so we need to delete it... const commands = selections.map(s => new ReplaceCommand(new Range(s.positionLineNumber, s.positionColumn, s.positionLineNumber, s.positionColumn + 1), '', false)); - return new EditOperationResult(EditOperationType.Typing, commands, { + return new EditOperationResult(EditOperationType.TypingOther, commands, { shouldPushStackElementBefore: true, shouldPushStackElementAfter: false }); @@ -824,7 +824,7 @@ export class TypeOperations { for (let i = 0, len = selections.length; i < len; i++) { commands[i] = TypeOperations._enter(config, model, false, selections[i]); } - return new EditOperationResult(EditOperationType.Typing, commands, { + return new EditOperationResult(EditOperationType.TypingOther, commands, { shouldPushStackElementBefore: true, shouldPushStackElementAfter: false, }); @@ -841,7 +841,7 @@ export class TypeOperations { } } if (!autoIndentFails) { - return new EditOperationResult(EditOperationType.Typing, commands, { + return new EditOperationResult(EditOperationType.TypingOther, commands, { shouldPushStackElementBefore: true, shouldPushStackElementAfter: false, }); @@ -877,12 +877,10 @@ export class TypeOperations { for (let i = 0, len = selections.length; i < len; i++) { commands[i] = new ReplaceCommand(selections[i], ch); } - let shouldPushStackElementBefore = (prevEditOperationType !== EditOperationType.Typing); - if (ch === ' ') { - shouldPushStackElementBefore = true; - } - return new EditOperationResult(EditOperationType.Typing, commands, { - shouldPushStackElementBefore: shouldPushStackElementBefore, + + const opType = getTypingOperation(ch, prevEditOperationType); + return new EditOperationResult(opType, commands, { + shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, opType), shouldPushStackElementAfter: false }); } @@ -892,8 +890,9 @@ export class TypeOperations { for (let i = 0, len = selections.length; i < len; i++) { commands[i] = new ReplaceCommand(selections[i], str); } - return new EditOperationResult(EditOperationType.Typing, commands, { - shouldPushStackElementBefore: (prevEditOperationType !== EditOperationType.Typing), + const opType = getTypingOperation(str, prevEditOperationType); + return new EditOperationResult(opType, commands, { + shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, opType), shouldPushStackElementAfter: false }); } @@ -965,3 +964,40 @@ export class TypeWithAutoClosingCommand extends ReplaceCommandWithOffsetCursorSt return super.computeCursorState(model, helper); } } + +function getTypingOperation(typedText: string, previousTypingOperation: EditOperationType): EditOperationType { + if (typedText === ' ') { + return previousTypingOperation === EditOperationType.TypingFirstSpace + || previousTypingOperation === EditOperationType.TypingConsecutiveSpace + ? EditOperationType.TypingConsecutiveSpace + : EditOperationType.TypingFirstSpace; + } + + return EditOperationType.TypingOther; +} + +function shouldPushStackElementBetween(previousTypingOperation: EditOperationType, typingOperation: EditOperationType): boolean { + if (isTypingOperation(previousTypingOperation) && !isTypingOperation(typingOperation)) { + // Always set an undo stop before non-type operations + return true; + } + if (previousTypingOperation === EditOperationType.TypingFirstSpace) { + // `abc |d`: No undo stop + // `abc |d`: Undo stop + return false; + } + // Insert undo stop between different operation types + return normalizeOperationType(previousTypingOperation) !== normalizeOperationType(typingOperation); +} + +function normalizeOperationType(type: EditOperationType): EditOperationType | 'space' { + return (type === EditOperationType.TypingConsecutiveSpace || type === EditOperationType.TypingFirstSpace) + ? 'space' + : type; +} + +function isTypingOperation(type: EditOperationType): boolean { + return type === EditOperationType.TypingOther + || type === EditOperationType.TypingFirstSpace + || type === EditOperationType.TypingConsecutiveSpace; +} diff --git a/src/vs/editor/common/core/lineTokens.ts b/src/vs/editor/common/core/lineTokens.ts index 816b865e90..5c149f1f4c 100644 --- a/src/vs/editor/common/core/lineTokens.ts +++ b/src/vs/editor/common/core/lineTokens.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ColorId, LanguageId, StandardTokenType, TokenMetadata } from 'vs/editor/common/modes'; +import { ColorId, FontStyle, LanguageId, MetadataConsts, StandardTokenType, TokenMetadata } from 'vs/editor/common/modes'; export interface IViewLineTokens { equals(other: IViewLineTokens): boolean; @@ -22,6 +22,20 @@ export class LineTokens implements IViewLineTokens { private readonly _tokensCount: number; private readonly _text: string; + public static createEmpty(lineContent: string): LineTokens { + const defaultMetadata = ( + (FontStyle.None << MetadataConsts.FONT_STYLE_OFFSET) + | (ColorId.DefaultForeground << MetadataConsts.FOREGROUND_OFFSET) + | (ColorId.DefaultBackground << MetadataConsts.BACKGROUND_OFFSET) + ) >>> 0; + + const tokens = new Uint32Array(2); + tokens[0] = lineContent.length; + tokens[1] = defaultMetadata; + + return new LineTokens(tokens, lineContent); + } + constructor(tokens: Uint32Array, text: string) { this._tokens = tokens; this._tokensCount = (this._tokens.length >>> 1); diff --git a/src/vs/editor/common/diff/diffComputer.ts b/src/vs/editor/common/diff/diffComputer.ts index 60b69baef1..04e5685b6e 100644 --- a/src/vs/editor/common/diff/diffComputer.ts +++ b/src/vs/editor/common/diff/diffComputer.ts @@ -45,6 +45,10 @@ class LineSequence implements ISequence { return elements; } + public getStrictElement(index: number): string { + return this.lines[index]; + } + public getStartLineNumber(i: number): number { return i + 1; } diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 5ad9df07bd..3a25559bfe 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -503,7 +503,7 @@ export interface IEditor { * Change the decorations. All decorations added through this changeAccessor * will get the ownerId of the editor (meaning they will not show up in other * editors). - * @see `ITextModel.changeDecorations` + * @see {@link ITextModel.changeDecorations} * @internal */ changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => any): any; @@ -639,6 +639,7 @@ export interface IContentDecorationRenderOptions { textDecoration?: string; color?: string | ThemeColor; backgroundColor?: string | ThemeColor; + opacity?: string; margin?: string; padding?: string; diff --git a/src/vs/editor/common/editorContextKeys.ts b/src/vs/editor/common/editorContextKeys.ts index 0148309a45..783bb43970 100644 --- a/src/vs/editor/common/editorContextKeys.ts +++ b/src/vs/editor/common/editorContextKeys.ts @@ -62,7 +62,7 @@ export namespace EditorContextKeys { export const hasReferenceProvider = new RawContextKey('editorHasReferenceProvider', false, nls.localize('editorHasReferenceProvider', "Whether the editor has a reference provider")); export const hasRenameProvider = new RawContextKey('editorHasRenameProvider', false, nls.localize('editorHasRenameProvider', "Whether the editor has a rename provider")); export const hasSignatureHelpProvider = new RawContextKey('editorHasSignatureHelpProvider', false, nls.localize('editorHasSignatureHelpProvider', "Whether the editor has a signature help provider")); - export const hasInlineHintsProvider = new RawContextKey('editorHasInlineHintsProvider', false, nls.localize('editorHasInlineHintsProvider', "Whether the editor has an inline hints provider")); + export const hasInlayHintsProvider = new RawContextKey('editorHasInlayHintsProvider', false, nls.localize('editorHasInlayHintsProvider', "Whether the editor has an inline hints provider")); // -- mode context keys: formatting export const hasDocumentFormattingProvider = new RawContextKey('editorHasDocumentFormattingProvider', false, nls.localize('editorHasDocumentFormattingProvider', "Whether the editor has a document formatting provider")); diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index f5f08cf0f5..5c8143e0ac 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -73,6 +73,11 @@ export interface IModelDecorationMinimapOptions extends IDecorationOptions { * Options for a model decoration. */ export interface IModelDecorationOptions { + /** + * A debug description that can be used for inspecting model decorations. + * @internal + */ + description: string; /** * Customize the growing behavior of the decoration when typing at the edges of the decoration. * Defaults to TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges @@ -1220,7 +1225,6 @@ export interface ITextModel { /** * An event emitted when the model has been attached to the first editor or detached from the last editor. * @event - * @internal */ onDidChangeAttached(listener: () => void): IDisposable; /** @@ -1247,7 +1251,6 @@ export interface ITextModel { /** * Returns if this model is attached to an editor or not. - * @internal */ isAttachedToEditor(): boolean; @@ -1256,6 +1259,33 @@ export interface ITextModel { * @internal */ getAttachedEditorCount(): number; + + /** + * Among all positions that are projected to the same position in the underlying text model as + * the given position, select a unique position as indicated by the affinity. + * @internal + */ + normalizePosition(position: Position, affinity: PositionNormalizationAffinity): Position; + + /** + * Gets the column at which indentation stops at a given line. + * @internal + */ + getLineIndentColumn(lineNumber: number): number; +} + +/** + * @internal + */ +export const enum PositionNormalizationAffinity { + /** + * Prefers the left most position. + */ + Left = 0, + /** + * Prefers the right most position. + */ + Right = 1, } /** diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 66a32bb6f2..882bff9658 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -3028,6 +3028,30 @@ export class TextModel extends Disposable implements model.ITextModel { } //#endregion + normalizePosition(position: Position, affinity: model.PositionNormalizationAffinity): Position { + return position; + } + + /** + * Gets the column at which indentation stops at a given line. + * @internal + */ + public getLineIndentColumn(lineNumber: number): number { + // Columns start with 1. + return indentOfLine(this.getLineContent(lineNumber)) + 1; + } +} + +function indentOfLine(line: string): number { + let indent = 0; + for (const c of line) { + if (c === ' ' || c === '\t') { + indent++; + } else { + break; + } + } + return indent; } //#region Decorations @@ -3205,6 +3229,7 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions { return new ModelDecorationOptions(options); } + readonly description: string; readonly stickiness: model.TrackedRangeStickiness; readonly zIndex: number; readonly className: string | null; @@ -3225,6 +3250,7 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions { readonly afterContentClassName: string | null; private constructor(options: model.IModelDecorationOptions) { + this.description = options.description; this.stickiness = options.stickiness || model.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges; this.zIndex = options.zIndex || 0; this.className = options.className ? cleanClassName(options.className) : null; @@ -3245,16 +3271,16 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions { this.afterContentClassName = options.afterContentClassName ? cleanClassName(options.afterContentClassName) : null; } } -ModelDecorationOptions.EMPTY = ModelDecorationOptions.register({}); +ModelDecorationOptions.EMPTY = ModelDecorationOptions.register({ description: 'empty' }); /** * The order carefully matches the values of the enum. */ const TRACKED_RANGE_OPTIONS = [ - ModelDecorationOptions.register({ stickiness: model.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges }), - ModelDecorationOptions.register({ stickiness: model.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }), - ModelDecorationOptions.register({ stickiness: model.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore }), - ModelDecorationOptions.register({ stickiness: model.TrackedRangeStickiness.GrowsOnlyWhenTypingAfter }), + ModelDecorationOptions.register({ description: 'tracked-range-always-grows-when-typing-at-edges', stickiness: model.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges }), + ModelDecorationOptions.register({ description: 'tracked-range-never-grows-when-typing-at-edges', stickiness: model.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }), + ModelDecorationOptions.register({ description: 'tracked-range-grows-only-when-typing-before', stickiness: model.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore }), + ModelDecorationOptions.register({ description: 'tracked-range-grows-only-when-typing-after', stickiness: model.TrackedRangeStickiness.GrowsOnlyWhenTypingAfter }), ]; function _normalizeOptions(options: model.IModelDecorationOptions): ModelDecorationOptions { diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index c07d10443b..adfdd52c39 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -9,7 +9,7 @@ import { Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { Position } from 'vs/editor/common/core/position'; +import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { TokenizationResult, TokenizationResult2 } from 'vs/editor/common/core/token'; @@ -227,7 +227,7 @@ export interface IState { } /** - * A provider result represents the values a provider, like the [`HoverProvider`](#HoverProvider), + * A provider result represents the values a provider, like the {@link HoverProvider}, * may return. For once this is the actual result type `T`, like `Hover`, or a thenable that resolves * to that type `T`. In addition, `null` and `undefined` can be returned - either directly or from a * thenable. @@ -557,13 +557,13 @@ export interface CompletionItem { documentation?: string | IMarkdownString; /** * A string that should be used when comparing this item - * with other items. When `falsy` the [label](#CompletionItem.label) + * with other items. When `falsy` the {@link CompletionItem.label label} * is used. */ sortText?: string; /** * A string that should be used when filtering a set of - * completion items. When `falsy` the [label](#CompletionItem.label) + * completion items. When `falsy` the {@link CompletionItem.label label} * is used. */ filterText?: string; @@ -587,11 +587,11 @@ export interface CompletionItem { /** * A range of text that should be replaced by this completion item. * - * Defaults to a range from the start of the [current word](#TextDocument.getWordRangeAtPosition) to the + * Defaults to a range from the start of the {@link TextDocument.getWordRangeAtPosition current word} to the * current position. * - * *Note:* The range must be a [single line](#Range.isSingleLine) and it must - * [contain](#Range.contains) the position at which completion has been [requested](#CompletionItemProvider.provideCompletionItems). + * *Note:* The range must be a {@link Range.isSingleLine single line} and it must + * {@link Range.contains contain} the position at which completion has been {@link CompletionItemProvider.provideCompletionItems requested}. */ range: IRange | { insert: IRange, replace: IRange }; /** @@ -638,7 +638,7 @@ export const enum CompletionTriggerKind { } /** * Contains additional information about the context in which - * [completion provider](#CompletionItemProvider.provideCompletionItems) is triggered. + * {@link CompletionItemProvider.provideCompletionItems completion provider} is triggered. */ export interface CompletionContext { /** @@ -658,10 +658,10 @@ export interface CompletionContext { * * When computing *complete* completion items is expensive, providers can optionally implement * the `resolveCompletionItem`-function. In that case it is enough to return completion - * items with a [label](#CompletionItem.label) from the - * [provideCompletionItems](#CompletionItemProvider.provideCompletionItems)-function. Subsequently, + * items with a {@link CompletionItem.label label} from the + * {@link CompletionItemProvider.provideCompletionItems provideCompletionItems}-function. Subsequently, * when a completion item is shown in the UI and gains focus this provider is asked to resolve - * the item, like adding [doc-comment](#CompletionItem.documentation) or [details](#CompletionItem.detail). + * the item, like adding {@link CompletionItem.documentation doc-comment} or {@link CompletionItem.detail details}. */ export interface CompletionItemProvider { @@ -677,14 +677,73 @@ export interface CompletionItemProvider { provideCompletionItems(model: model.ITextModel, position: Position, context: CompletionContext, token: CancellationToken): ProviderResult; /** - * Given a completion item fill in more data, like [doc-comment](#CompletionItem.documentation) - * or [details](#CompletionItem.detail). + * Given a completion item fill in more data, like {@link CompletionItem.documentation doc-comment} + * or {@link CompletionItem.detail details}. * * The editor will only resolve a completion item once. */ resolveCompletionItem?(item: CompletionItem, token: CancellationToken): ProviderResult; } +/** + * How an {@link InlineCompletionsProvider inline completion provider} was triggered. + */ +export enum InlineCompletionTriggerKind { + /** + * Completion was triggered automatically while editing. + * It is sufficient to return a single completion item in this case. + */ + Automatic = 0, + + /** + * Completion was triggered explicitly by a user gesture. + * Return multiple completion items to enable cycling through them. + */ + Explicit = 1, +} + +export interface InlineCompletionContext { + /** + * How the completion was triggered. + */ + readonly triggerKind: InlineCompletionTriggerKind; +} + +export interface InlineCompletion { + /** + * The text to insert. + * If the text contains a line break, the range must end at the end of a line. + * If existing text should be replaced, the existing text must be a prefix of the text to insert. + */ + readonly text: string; + + /** + * The range to replace. + * Must begin and end on the same line. + */ + readonly range?: IRange; + + readonly command?: Command; +} + +export interface InlineCompletions { + readonly items: readonly TItem[]; +} + +export interface InlineCompletionsProvider { + provideInlineCompletions(model: model.ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult; + + /** + * Will be called when an item is shown. + */ + handleItemDidShow?(completions: T, item: T['items'][number]): void; + + /** + * Will be called when a completions list is no longer in use and can be garbage-collected. + */ + freeInlineCompletions(completions: T): void; +} + export interface CodeAction { title: string; command?: Command; @@ -870,7 +929,7 @@ export interface DocumentHighlight { */ range: IRange; /** - * The highlight kind, default is [text](#DocumentHighlightKind.Text). + * The highlight kind, default is {@link DocumentHighlightKind.Text text}. */ kind?: DocumentHighlightKind; } @@ -1323,12 +1382,12 @@ export interface IColorPresentation { */ label: string; /** - * An [edit](#TextEdit) which is applied to a document when selecting + * An {@link TextEdit edit} which is applied to a document when selecting * this presentation for the color. */ textEdit?: TextEdit; /** - * An optional array of additional [text edits](#TextEdit) that are applied when + * An optional array of additional {@link TextEdit text edits} that are applied when * selecting this color presentation. */ additionalTextEdits?: TextEdit[]; @@ -1406,10 +1465,10 @@ export interface FoldingRange { end: number; /** - * Describes the [Kind](#FoldingRangeKind) of the folding range such as [Comment](#FoldingRangeKind.Comment) or - * [Region](#FoldingRangeKind.Region). The kind is used to categorize folding ranges and used by commands + * Describes the {@link FoldingRangeKind Kind} of the folding range such as {@link FoldingRangeKind.Comment Comment} or + * {@link FoldingRangeKind.Region Region}. The kind is used to categorize folding ranges and used by commands * like 'Fold all comments'. See - * [FoldingRangeKind](#FoldingRangeKind) for an enumeration of standardized kinds. + * {@link FoldingRangeKind} for an enumeration of standardized kinds. */ kind?: FoldingRangeKind; } @@ -1429,7 +1488,7 @@ export class FoldingRangeKind { static readonly Region = new FoldingRangeKind('region'); /** - * Creates a new [FoldingRangeKind](#FoldingRangeKind). + * Creates a new {@link FoldingRangeKind}. * * @param value of the kind. */ @@ -1701,24 +1760,23 @@ export interface CodeLensProvider { } -export enum InlineHintKind { +export enum InlayHintKind { Other = 0, Type = 1, Parameter = 2, } -export interface InlineHint { +export interface InlayHint { text: string; - range: IRange; - kind: InlineHintKind; - description?: string | IMarkdownString; + position: IPosition; + kind: InlayHintKind; whitespaceBefore?: boolean; whitespaceAfter?: boolean; } -export interface InlineHintsProvider { - onDidChangeInlineHints?: Event | undefined; - provideInlineHints(model: model.ITextModel, range: Range, token: CancellationToken): ProviderResult; +export interface InlayHintsProvider { + onDidChangeInlayHints?: Event | undefined; + provideInlayHints(model: model.ITextModel, range: Range, token: CancellationToken): ProviderResult; } export interface SemanticTokensLegend { @@ -1771,6 +1829,11 @@ export const RenameProviderRegistry = new LanguageFeatureRegistry(); +/** + * @internal + */ +export const InlineCompletionsProviderRegistry = new LanguageFeatureRegistry(); + /** * @internal */ @@ -1834,7 +1897,7 @@ export const CodeLensProviderRegistry = new LanguageFeatureRegistry(); +export const InlayHintsProviderRegistry = new LanguageFeatureRegistry(); /** * @internal diff --git a/src/vs/editor/common/modes/linkComputer.ts b/src/vs/editor/common/modes/linkComputer.ts index d9091b415e..f82cf29339 100644 --- a/src/vs/editor/common/modes/linkComputer.ts +++ b/src/vs/editor/common/modes/linkComputer.ts @@ -154,7 +154,7 @@ function getClassifier(): CharacterClassifier { if (_classifier === null) { _classifier = new CharacterClassifier(CharacterClass.None); - const FORCE_TERMINATION_CHARACTERS = ' \t<>\'\"、。。、,.:;‘“〈《「『【〔([{「」}])〕】』」》〉”’`~…'; + const FORCE_TERMINATION_CHARACTERS = ' \t<>\'\"、。。、,.:;‘〈「『〔([{「」}])〕』」〉’`~…'; for (let i = 0; i < FORCE_TERMINATION_CHARACTERS.length; i++) { _classifier.set(FORCE_TERMINATION_CHARACTERS.charCodeAt(i), CharacterClass.ForceTermination); } diff --git a/src/vs/editor/common/modes/supports/onEnter.ts b/src/vs/editor/common/modes/supports/onEnter.ts index 6edd37e29d..1b2f0eb4c0 100644 --- a/src/vs/editor/common/modes/supports/onEnter.ts +++ b/src/vs/editor/common/modes/supports/onEnter.ts @@ -64,7 +64,12 @@ export class OnEnterSupport { reg: rule.previousLineText, text: previousLineText }].every((obj): boolean => { - return obj.reg ? obj.reg.test(obj.text) : true; + if (!obj.reg) { + return true; + } + + obj.reg.lastIndex = 0; // To disable the effect of the "g" flag. + return obj.reg.test(obj.text); }); if (regResult) { diff --git a/src/vs/editor/common/services/markerDecorationsServiceImpl.ts b/src/vs/editor/common/services/markerDecorationsServiceImpl.ts index 8251d31139..16a62ce13e 100644 --- a/src/vs/editor/common/services/markerDecorationsServiceImpl.ts +++ b/src/vs/editor/common/services/markerDecorationsServiceImpl.ts @@ -239,6 +239,7 @@ export class MarkerDecorationsService extends Disposable implements IMarkerDecor } return { + description: 'marker-decoration', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className, showIfCollapsed: true, diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 461e4d403c..57099fd138 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -219,82 +219,84 @@ export enum EditorOption { highlightActiveIndentGuide = 49, hover = 50, inDiffEditor = 51, - letterSpacing = 52, - lightbulb = 53, - lineDecorationsWidth = 54, - lineHeight = 55, - lineNumbers = 56, - lineNumbersMinChars = 57, - linkedEditing = 58, - links = 59, - matchBrackets = 60, - minimap = 61, - mouseStyle = 62, - mouseWheelScrollSensitivity = 63, - mouseWheelZoom = 64, - multiCursorMergeOverlapping = 65, - multiCursorModifier = 66, - multiCursorPaste = 67, - occurrencesHighlight = 68, - overviewRulerBorder = 69, - overviewRulerLanes = 70, - padding = 71, - parameterHints = 72, - peekWidgetDefaultFocus = 73, - definitionLinkOpensInPeek = 74, - quickSuggestions = 75, - quickSuggestionsDelay = 76, - readOnly = 77, - renameOnType = 78, - renderControlCharacters = 79, - renderIndentGuides = 80, - renderFinalNewline = 81, - renderLineHighlight = 82, - renderLineHighlightOnlyWhenFocus = 83, - renderValidationDecorations = 84, - renderWhitespace = 85, - revealHorizontalRightPadding = 86, - roundedSelection = 87, - rulers = 88, - scrollbar = 89, - scrollBeyondLastColumn = 90, - scrollBeyondLastLine = 91, - scrollPredominantAxis = 92, - selectionClipboard = 93, - selectionHighlight = 94, - selectOnLineNumbers = 95, - showFoldingControls = 96, - showUnused = 97, - snippetSuggestions = 98, - smartSelect = 99, - smoothScrolling = 100, - stickyTabStops = 101, - stopRenderingLineAfter = 102, - suggest = 103, - suggestFontSize = 104, - suggestLineHeight = 105, - suggestOnTriggerCharacters = 106, - suggestSelection = 107, - tabCompletion = 108, - tabIndex = 109, - unusualLineTerminators = 110, - useTabStops = 111, - wordSeparators = 112, - wordWrap = 113, - wordWrapBreakAfterCharacters = 114, - wordWrapBreakBeforeCharacters = 115, - wordWrapColumn = 116, - wordWrapOverride1 = 117, - wordWrapOverride2 = 118, - wrappingIndent = 119, - wrappingStrategy = 120, - showDeprecated = 121, - inlineHints = 122, - editorClassName = 123, - pixelRatio = 124, - tabFocusMode = 125, - layoutInfo = 126, - wrappingInfo = 127 + inlineSuggest = 52, + letterSpacing = 53, + lightbulb = 54, + lineDecorationsWidth = 55, + lineHeight = 56, + lineNumbers = 57, + lineNumbersMinChars = 58, + linkedEditing = 59, + links = 60, + matchBrackets = 61, + minimap = 62, + mouseStyle = 63, + mouseWheelScrollSensitivity = 64, + mouseWheelZoom = 65, + multiCursorMergeOverlapping = 66, + multiCursorModifier = 67, + multiCursorPaste = 68, + occurrencesHighlight = 69, + overviewRulerBorder = 70, + overviewRulerLanes = 71, + padding = 72, + parameterHints = 73, + peekWidgetDefaultFocus = 74, + definitionLinkOpensInPeek = 75, + quickSuggestions = 76, + quickSuggestionsDelay = 77, + readOnly = 78, + renameOnType = 79, + renderControlCharacters = 80, + renderIndentGuides = 81, + renderFinalNewline = 82, + renderLineHighlight = 83, + renderLineHighlightOnlyWhenFocus = 84, + renderValidationDecorations = 85, + renderWhitespace = 86, + revealHorizontalRightPadding = 87, + roundedSelection = 88, + rulers = 89, + scrollbar = 90, + scrollBeyondLastColumn = 91, + scrollBeyondLastLine = 92, + scrollPredominantAxis = 93, + selectionClipboard = 94, + selectionHighlight = 95, + selectOnLineNumbers = 96, + showFoldingControls = 97, + showUnused = 98, + snippetSuggestions = 99, + smartSelect = 100, + smoothScrolling = 101, + stickyTabStops = 102, + stopRenderingLineAfter = 103, + suggest = 104, + suggestFontSize = 105, + suggestLineHeight = 106, + suggestOnTriggerCharacters = 107, + suggestSelection = 108, + tabCompletion = 109, + tabIndex = 110, + unusualLineTerminators = 111, + useShadowDOM = 112, + useTabStops = 113, + wordSeparators = 114, + wordWrap = 115, + wordWrapBreakAfterCharacters = 116, + wordWrapBreakBeforeCharacters = 117, + wordWrapColumn = 118, + wordWrapOverride1 = 119, + wordWrapOverride2 = 120, + wrappingIndent = 121, + wrappingStrategy = 122, + showDeprecated = 123, + inlayHints = 124, + editorClassName = 125, + pixelRatio = 126, + tabFocusMode = 127, + layoutInfo = 128, + wrappingInfo = 129 } /** @@ -353,12 +355,28 @@ export enum IndentAction { Outdent = 3 } -export enum InlineHintKind { +export enum InlayHintKind { Other = 0, Type = 1, Parameter = 2 } +/** + * How an {@link InlineCompletionsProvider inline completion provider} was triggered. + */ +export enum InlineCompletionTriggerKind { + /** + * Completion was triggered automatically while editing. + * It is sufficient to return a single completion item in this case. + */ + Automatic = 0, + /** + * Completion was triggered explicitly by a user gesture. + * Return multiple completion items to enable cycling through them. + */ + Explicit = 1 +} + /** * Virtual Key Codes, the value does not hold any inherent meaning. * Inspired somewhat from https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx @@ -823,4 +841,4 @@ export enum WrappingIndent { * DeepIndent => wrapped lines get +2 indentation toward the parent. */ DeepIndent = 3 -} \ No newline at end of file +} diff --git a/src/vs/editor/common/view/editorColorRegistry.ts b/src/vs/editor/common/view/editorColorRegistry.ts index 52bbde9eef..aab19b2943 100644 --- a/src/vs/editor/common/view/editorColorRegistry.ts +++ b/src/vs/editor/common/view/editorColorRegistry.ts @@ -43,6 +43,9 @@ export const editorGutter = registerColor('editorGutter.background', { dark: edi export const editorUnnecessaryCodeBorder = registerColor('editorUnnecessaryCode.border', { dark: null, light: null, hc: Color.fromHex('#fff').transparent(0.8) }, nls.localize('unnecessaryCodeBorder', 'Border color of unnecessary (unused) source code in the editor.')); export const editorUnnecessaryCodeOpacity = registerColor('editorUnnecessaryCode.opacity', { dark: Color.fromHex('#000a'), light: Color.fromHex('#0007'), hc: null }, nls.localize('unnecessaryCodeOpacity', 'Opacity of unnecessary (unused) source code in the editor. For example, "#000000c0" will render the code with 75% opacity. For high contrast themes, use the \'editorUnnecessaryCode.border\' theme color to underline unnecessary code instead of fading it out.')); +export const ghostTextBorder = registerColor('editorGhostText.border', { dark: null, light: null, hc: Color.fromHex('#fff').transparent(0.8) }, nls.localize('editorGhostTextBorder', 'Border color of ghost text in the editor.')); +export const ghostTextForeground = registerColor('editorGhostText.foreground', { dark: Color.fromHex('#ffffff56'), light: Color.fromHex('#0007'), hc: null }, nls.localize('editorGhostTextForeground', 'Foreground color of the ghost text in the editor.')); + const rulerRangeDefault = new Color(new RGBA(0, 122, 204, 0.6)); export const overviewRulerRangeHighlight = registerColor('editorOverviewRuler.rangeHighlightForeground', { dark: rulerRangeDefault, light: rulerRangeDefault, hc: rulerRangeDefault }, nls.localize('overviewRulerRangeHighlight', 'Overview ruler marker color for range highlights. The color must not be opaque so as not to hide underlying decorations.'), true); export const overviewRulerError = registerColor('editorOverviewRuler.errorForeground', { dark: new Color(new RGBA(255, 18, 18, 0.7)), light: new Color(new RGBA(255, 18, 18, 0.7)), hc: new Color(new RGBA(255, 50, 50, 1)) }, nls.localize('overviewRuleError', 'Overview ruler marker color for errors.')); diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index 67dbc0c15a..a4463d1ee7 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -47,6 +47,10 @@ class LinePart { public isWhitespace(): boolean { return (this.metadata & LinePartMetadata.IS_WHITESPACE_MASK ? true : false); } + + public isPseudoAfter(): boolean { + return (this.metadata & LinePartMetadata.PSEUDO_AFTER_MASK ? true : false); + } } export class LineRange { @@ -213,16 +217,23 @@ export const enum CharacterMappingConstants { PART_INDEX_OFFSET = 16 } +export class DomPosition { + constructor( + public readonly partIndex: number, + public readonly charIndex: number + ) { } +} + /** * Provides a both direction mapping between a line's character and its rendered position. */ export class CharacterMapping { - public static getPartIndex(partData: number): number { + private static getPartIndex(partData: number): number { return (partData & CharacterMappingConstants.PART_INDEX_MASK) >>> CharacterMappingConstants.PART_INDEX_OFFSET; } - public static getCharIndex(partData: number): number { + private static getCharIndex(partData: number): number { return (partData & CharacterMappingConstants.CHAR_INDEX_MASK) >>> CharacterMappingConstants.CHAR_INDEX_OFFSET; } @@ -236,20 +247,24 @@ export class CharacterMapping { this._absoluteOffsets = new Uint32Array(this.length); } - public setPartData(charOffset: number, partIndex: number, charIndex: number, partAbsoluteOffset: number): void { - let partData = ( + public setColumnInfo(column: number, partIndex: number, charIndex: number, partAbsoluteOffset: number): void { + const partData = ( (partIndex << CharacterMappingConstants.PART_INDEX_OFFSET) | (charIndex << CharacterMappingConstants.CHAR_INDEX_OFFSET) ) >>> 0; - this._data[charOffset] = partData; - this._absoluteOffsets[charOffset] = partAbsoluteOffset + charIndex; + this._data[column - 1] = partData; + this._absoluteOffsets[column - 1] = partAbsoluteOffset + charIndex; } - public getAbsoluteOffsets(): Uint32Array { - return this._absoluteOffsets; + public getAbsoluteOffset(column: number): number { + if (this._absoluteOffsets.length === 0) { + // No characters on this line + return 0; + } + return this._absoluteOffsets[column - 1]; } - public charOffsetToPartData(charOffset: number): number { + private charOffsetToPartData(charOffset: number): number { if (this.length === 0) { return 0; } @@ -262,7 +277,19 @@ export class CharacterMapping { return this._data[charOffset]; } - public partDataToCharOffset(partIndex: number, partLength: number, charIndex: number): number { + public getDomPosition(column: number): DomPosition { + const partData = this.charOffsetToPartData(column - 1); + const partIndex = CharacterMapping.getPartIndex(partData); + const charIndex = CharacterMapping.getCharIndex(partData); + return new DomPosition(partIndex, charIndex); + } + + public getColumn(domPosition: DomPosition, partLength: number): number { + const charOffset = this.partDataToCharOffset(domPosition.partIndex, partLength, domPosition.charIndex); + return charOffset + 1; + } + + private partDataToCharOffset(partIndex: number, partLength: number, charIndex: number): number { if (this.length === 0) { return 0; } @@ -373,7 +400,7 @@ export function renderViewLine(input: RenderLineInput, sb: IStringBuilder): Rend sb.appendASCIIString(``); const characterMapping = new CharacterMapping(1, beforeCount + afterCount); - characterMapping.setPartData(0, beforeCount, 0, 0); + characterMapping.setColumnInfo(1, beforeCount, 0, 0); return new RenderLineOutput( characterMapping, @@ -792,14 +819,11 @@ function _applyInlineDecorations(lineContent: string, len: number, tokens: LineP const lastTokenEndIndex = tokens[tokens.length - 1].endIndex; if (lineDecorationIndex < lineDecorationsLen && lineDecorations[lineDecorationIndex].startOffset === lastTokenEndIndex) { - let classNames: string[] = []; - let metadata = 0; while (lineDecorationIndex < lineDecorationsLen && lineDecorations[lineDecorationIndex].startOffset === lastTokenEndIndex) { - classNames.push(lineDecorations[lineDecorationIndex].className); - metadata |= lineDecorations[lineDecorationIndex].metadata; + const lineDecoration = lineDecorations[lineDecorationIndex]; + result[resultLen++] = new LinePart(lastResultEndIndex, lineDecoration.className, lineDecoration.metadata); lineDecorationIndex++; } - result[resultLen++] = new LinePart(lastResultEndIndex, classNames.join(' '), metadata); } return result; @@ -827,6 +851,7 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render const renderControlCharacters = input.renderControlCharacters; const characterMapping = new CharacterMapping(len + 1, parts.length); + let lastCharacterMappingDefined = false; let charIndex = 0; let visibleColumn = startVisibleColumn; @@ -850,7 +875,7 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render const partType = part.type; const partRendersWhitespace = (renderWhitespace !== RenderWhitespace.None && part.isWhitespace()); const partRendersWhitespaceWithWidth = partRendersWhitespace && !fontIsMonospace && (partType === 'mtkw'/*only whitespace*/ || !containsForeignElements); - const partIsEmptyAndHasPseudoAfter = (charIndex === partEndIndex && part.metadata === LinePartMetadata.PSEUDO_AFTER); + const partIsEmptyAndHasPseudoAfter = (charIndex === partEndIndex && part.isPseudoAfter()); charOffsetInPart = 0; sb.appendASCIIString(' anchor, getActions: () => menuActions, onHide: () => { diff --git a/src/vs/editor/contrib/codelens/codelensController.ts b/src/vs/editor/contrib/codelens/codelensController.ts index 933fb0e8ba..194f0e89d0 100644 --- a/src/vs/editor/contrib/codelens/codelensController.ts +++ b/src/vs/editor/contrib/codelens/codelensController.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { CancelablePromise, RunOnceScheduler, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; diff --git a/src/vs/editor/contrib/colorPicker/colorDetector.ts b/src/vs/editor/contrib/colorPicker/colorDetector.ts index 0b54b39776..763c18835f 100644 --- a/src/vs/editor/contrib/colorPicker/colorDetector.ts +++ b/src/vs/editor/contrib/colorPicker/colorDetector.ts @@ -178,7 +178,7 @@ export class ColorDetector extends Disposable implements IEditorContribution { let key = 'colorBox-' + subKey; if (!this._decorationsTypes.has(key) && !newDecorationsTypes[key]) { - this._codeEditorService.registerDecorationType(key, { + this._codeEditorService.registerDecorationType('color-detector-color', key, { before: { contentText: ' ', border: 'solid 0.1em #000', diff --git a/src/vs/editor/contrib/comment/comment.ts b/src/vs/editor/contrib/comment/comment.ts index 366a72c91c..760534017c 100644 --- a/src/vs/editor/contrib/comment/comment.ts +++ b/src/vs/editor/contrib/comment/comment.ts @@ -12,7 +12,7 @@ import { ICommand } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { BlockCommentCommand } from 'vs/editor/contrib/comment/blockCommentCommand'; import { LineCommentCommand, Type } from 'vs/editor/contrib/comment/lineCommentCommand'; -// import { MenuId } from 'vs/platform/actions/common/actions'; +// import { MenuId } from 'vs/platform/actions/common/actions'; {{SQL CARBON EDIT}} import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; diff --git a/src/vs/editor/contrib/contextmenu/contextmenu.ts b/src/vs/editor/contrib/contextmenu/contextmenu.ts index 2f34979791..98457591cf 100644 --- a/src/vs/editor/contrib/contextmenu/contextmenu.ts +++ b/src/vs/editor/contrib/contextmenu/contextmenu.ts @@ -23,6 +23,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { isIOS } from 'vs/base/common/platform'; export class ContextMenuController implements IEditorContribution { @@ -135,7 +136,8 @@ export class ContextMenuController implements IEditorContribution { } // Find actions available for menu - const menuActions = this._getMenuActions(this._editor.getModel(), MenuId.EditorContext); + const menuActions = this._getMenuActions(this._editor.getModel(), + this._editor.isSimpleWidget ? MenuId.SimpleEditorContext : MenuId.EditorContext); // Show menu if we have actions to show if (menuActions.length > 0) { @@ -208,10 +210,12 @@ export class ContextMenuController implements IEditorContribution { anchor = { x: posx, y: posy }; } + const useShadowDOM = this._editor.getOption(EditorOption.useShadowDOM) && !isIOS; // Do not use shadow dom on IOS #122035 + // Show menu this._contextMenuIsBeingShownCount++; this._contextMenuService.showContextMenu({ - domForShadowRoot: this._editor.getDomNode(), + domForShadowRoot: useShadowDOM ? this._editor.getDomNode() : undefined, getAnchor: () => anchor!, diff --git a/src/vs/editor/contrib/dnd/dnd.css b/src/vs/editor/contrib/dnd/dnd.css index 8ec41c2bbc..8219eb1867 100644 --- a/src/vs/editor/contrib/dnd/dnd.css +++ b/src/vs/editor/contrib/dnd/dnd.css @@ -25,4 +25,4 @@ .monaco-editor.vs-dark.mac.mouse-copy .view-lines, .monaco-editor.hc-black.mac.mouse-copy .view-lines { cursor: copy; -} \ No newline at end of file +} diff --git a/src/vs/editor/contrib/dnd/dnd.ts b/src/vs/editor/contrib/dnd/dnd.ts index e6369a67b0..85916d6d76 100644 --- a/src/vs/editor/contrib/dnd/dnd.ts +++ b/src/vs/editor/contrib/dnd/dnd.ts @@ -204,6 +204,7 @@ export class DragAndDropController extends Disposable implements IEditorContribu } private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({ + description: 'dnd-target', className: 'dnd-target' }); diff --git a/src/vs/editor/contrib/find/findController.ts b/src/vs/editor/contrib/find/findController.ts index 4d5f6d763a..9e4441da74 100644 --- a/src/vs/editor/contrib/find/findController.ts +++ b/src/vs/editor/contrib/find/findController.ts @@ -26,7 +26,6 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { IThemeService } from 'vs/platform/theme/common/themeService'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; const SEARCH_STRING_MAX_LENGTH = 524288; @@ -502,12 +501,7 @@ export const StartFindAction = registerMultiEditorAction(new MultiEditorAction({ } })); -StartFindAction.addImplementation(0, (accessor: ServicesAccessor, args: any): boolean | Promise => { - const codeEditorService = accessor.get(ICodeEditorService); - const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor(); - if (!editor) { - return false; - } +StartFindAction.addImplementation(0, (accessor: ServicesAccessor, editor: ICodeEditor, args: any): boolean | Promise => { const controller = CommonFindController.get(editor); if (!controller) { return false; @@ -587,39 +581,16 @@ export class NextMatchFindAction extends MatchFindAction { label: nls.localize('findNextMatchAction', "Find Next"), alias: 'Find Next', precondition: undefined, - kbOpts: { + kbOpts: [{ kbExpr: EditorContextKeys.focus, primary: KeyCode.F3, mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_G, secondary: [KeyCode.F3] }, weight: KeybindingWeight.EditorContrib - } - }); - } - - protected _run(controller: CommonFindController): boolean { - const result = controller.moveToNextMatch(); - if (result) { - controller.editor.pushUndoStop(); - return true; - } - - return false; - } -} - -export class NextMatchFindAction2 extends MatchFindAction { - - constructor() { - super({ - id: FIND_IDS.NextMatchFindAction, - label: nls.localize('findNextMatchAction', "Find Next"), - alias: 'Find Next', - precondition: undefined, - kbOpts: { + }, { kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_FIND_INPUT_FOCUSED), primary: KeyCode.Enter, weight: KeybindingWeight.EditorContrib - } + }] }); } @@ -642,33 +613,17 @@ export class PreviousMatchFindAction extends MatchFindAction { label: nls.localize('findPreviousMatchAction', "Find Previous"), alias: 'Find Previous', precondition: undefined, - kbOpts: { + kbOpts: [{ kbExpr: EditorContextKeys.focus, primary: KeyMod.Shift | KeyCode.F3, mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G, secondary: [KeyMod.Shift | KeyCode.F3] }, weight: KeybindingWeight.EditorContrib - } - }); - } - - protected _run(controller: CommonFindController): boolean { - return controller.moveToPrevMatch(); - } -} - -export class PreviousMatchFindAction2 extends MatchFindAction { - - constructor() { - super({ - id: FIND_IDS.PreviousMatchFindAction, - label: nls.localize('findPreviousMatchAction', "Find Previous"), - alias: 'Find Previous', - precondition: undefined, - kbOpts: { + }, { kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_FIND_INPUT_FOCUSED), primary: KeyMod.Shift | KeyCode.Enter, weight: KeybindingWeight.EditorContrib } + ] }); } @@ -765,10 +720,8 @@ export const StartFindReplaceAction = registerMultiEditorAction(new MultiEditorA } })); -StartFindReplaceAction.addImplementation(0, (accessor: ServicesAccessor, args: any): boolean | Promise => { - const codeEditorService = accessor.get(ICodeEditorService); - const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor(); - if (!editor || !editor.hasModel() || editor.getOption(EditorOption.readOnly)) { +StartFindReplaceAction.addImplementation(0, (accessor: ServicesAccessor, editor: ICodeEditor, args: any): boolean | Promise => { + if (!editor.hasModel() || editor.getOption(EditorOption.readOnly)) { return false; } const controller = CommonFindController.get(editor); @@ -808,9 +761,7 @@ registerEditorContribution(CommonFindController.ID, FindController); registerEditorAction(StartFindWithSelectionAction); registerEditorAction(NextMatchFindAction); -registerEditorAction(NextMatchFindAction2); registerEditorAction(PreviousMatchFindAction); -registerEditorAction(PreviousMatchFindAction2); registerEditorAction(NextSelectionMatchFindAction); registerEditorAction(PreviousSelectionMatchFindAction); diff --git a/src/vs/editor/contrib/find/findDecorations.ts b/src/vs/editor/contrib/find/findDecorations.ts index ce4d783280..f9313f0a24 100644 --- a/src/vs/editor/contrib/find/findDecorations.ts +++ b/src/vs/editor/contrib/find/findDecorations.ts @@ -276,6 +276,7 @@ export class FindDecorations implements IDisposable { } public static readonly _CURRENT_FIND_MATCH_DECORATION = ModelDecorationOptions.register({ + description: 'current-find-match', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, zIndex: 13, className: 'currentFindMatch', @@ -291,6 +292,7 @@ export class FindDecorations implements IDisposable { }); public static readonly _FIND_MATCH_DECORATION = ModelDecorationOptions.register({ + description: 'find-match', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'findMatch', showIfCollapsed: true, @@ -305,12 +307,14 @@ export class FindDecorations implements IDisposable { }); public static readonly _FIND_MATCH_NO_OVERVIEW_DECORATION = ModelDecorationOptions.register({ + description: 'find-match-no-overview', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'findMatch', showIfCollapsed: true }); private static readonly _FIND_MATCH_ONLY_OVERVIEW_DECORATION = ModelDecorationOptions.register({ + description: 'find-match-only-overview', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, overviewRuler: { color: themeColorFromId(overviewRulerFindMatchForeground), @@ -319,12 +323,14 @@ export class FindDecorations implements IDisposable { }); private static readonly _RANGE_HIGHLIGHT_DECORATION = ModelDecorationOptions.register({ + description: 'find-range-highlight', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'rangeHighlight', isWholeLine: true }); private static readonly _FIND_SCOPE_DECORATION = ModelDecorationOptions.register({ + description: 'find-scope', className: 'findScope', isWholeLine: true }); diff --git a/src/vs/editor/contrib/find/test/findController.test.ts b/src/vs/editor/contrib/find/test/findController.test.ts index 04169c0454..dee8039b8c 100644 --- a/src/vs/editor/contrib/find/test/findController.test.ts +++ b/src/vs/editor/contrib/find/test/findController.test.ts @@ -63,7 +63,7 @@ function executeAction(instantiationService: IInstantiationService, editor: ICod }); } -suite.skip('FindController', async () => { +suite.skip('FindController', async () => { // {{SQL CARBON EDIT}} Skip suite const queryState: { [key: string]: any; } = {}; let clipboardState = ''; const serviceCollection = new ServiceCollection(); @@ -490,7 +490,7 @@ suite.skip('FindController', async () => { }); }); -suite.skip('FindController query options persistence', async () => { +suite.skip('FindController query options persistence', async () => { // {{SQL CARBON EDIT}} Skip suite let queryState: { [key: string]: any; } = {}; queryState['editor.isRegex'] = false; queryState['editor.matchCase'] = false; diff --git a/src/vs/editor/contrib/folding/folding.ts b/src/vs/editor/contrib/folding/folding.ts index 142227ff45..ff75362a0d 100644 --- a/src/vs/editor/contrib/folding/folding.ts +++ b/src/vs/editor/contrib/folding/folding.ts @@ -51,7 +51,7 @@ interface FoldingStateMemento { export class FoldingController extends Disposable implements IEditorContribution { - public static ID = 'editor.contrib.folding'; + public static readonly ID = 'editor.contrib.folding'; static readonly MAX_FOLDING_REGIONS = 5000; diff --git a/src/vs/editor/contrib/folding/foldingDecorations.ts b/src/vs/editor/contrib/folding/foldingDecorations.ts index b226cf6e63..9abf14611d 100644 --- a/src/vs/editor/contrib/folding/foldingDecorations.ts +++ b/src/vs/editor/contrib/folding/foldingDecorations.ts @@ -17,6 +17,7 @@ export const foldingCollapsedIcon = registerIcon('folding-collapsed', Codicon.ch export class FoldingDecorationProvider implements IDecorationProvider { private static readonly COLLAPSED_VISUAL_DECORATION = ModelDecorationOptions.register({ + description: 'folding-collapsed-visual-decoration', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, afterContentClassName: 'inline-folded', isWholeLine: true, @@ -24,6 +25,7 @@ export class FoldingDecorationProvider implements IDecorationProvider { }); private static readonly COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION = ModelDecorationOptions.register({ + description: 'folding-collapsed-highlighted-visual-decoration', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, afterContentClassName: 'inline-folded', className: 'folded-background', @@ -32,18 +34,21 @@ export class FoldingDecorationProvider implements IDecorationProvider { }); private static readonly EXPANDED_AUTO_HIDE_VISUAL_DECORATION = ModelDecorationOptions.register({ + description: 'folding-expanded-auto-hide-visual-decoration', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, isWholeLine: true, firstLineDecorationClassName: ThemeIcon.asClassName(foldingExpandedIcon) }); private static readonly EXPANDED_VISUAL_DECORATION = ModelDecorationOptions.register({ + description: 'folding-expanded-visual-decoration', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, isWholeLine: true, firstLineDecorationClassName: 'alwaysShowFoldIcons ' + ThemeIcon.asClassName(foldingExpandedIcon) }); private static readonly HIDDEN_RANGE_DECORATION = ModelDecorationOptions.register({ + description: 'folding-hidden-range-decoration', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }); diff --git a/src/vs/editor/contrib/folding/intializingRangeProvider.ts b/src/vs/editor/contrib/folding/intializingRangeProvider.ts index f562e1d7cd..5a0ea0c8dd 100644 --- a/src/vs/editor/contrib/folding/intializingRangeProvider.ts +++ b/src/vs/editor/contrib/folding/intializingRangeProvider.ts @@ -28,6 +28,7 @@ export class InitializingRangeProvider implements RangeProvider { endColumn: editorModel.getLineLength(range.endLineNumber) }, options: { + description: 'folding-initializing-range-provider', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } }; diff --git a/src/vs/editor/contrib/folding/test/foldingModel.test.ts b/src/vs/editor/contrib/folding/test/foldingModel.test.ts index 9ddf7b0bc5..8bcacf285a 100644 --- a/src/vs/editor/contrib/folding/test/foldingModel.test.ts +++ b/src/vs/editor/contrib/folding/test/foldingModel.test.ts @@ -29,16 +29,19 @@ interface ExpectedDecoration { export class TestDecorationProvider { private static readonly collapsedDecoration = ModelDecorationOptions.register({ + description: 'test', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, linesDecorationsClassName: 'folding' }); private static readonly expandedDecoration = ModelDecorationOptions.register({ + description: 'test', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, linesDecorationsClassName: 'folding' }); private static readonly hiddenDecoration = ModelDecorationOptions.register({ + description: 'test', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, linesDecorationsClassName: 'folding' }); diff --git a/src/vs/editor/contrib/gotoSymbol/goToCommands.ts b/src/vs/editor/contrib/gotoSymbol/goToCommands.ts index be3a347cba..24ca59345c 100644 --- a/src/vs/editor/contrib/gotoSymbol/goToCommands.ts +++ b/src/vs/editor/contrib/gotoSymbol/goToCommands.ts @@ -190,7 +190,7 @@ abstract class SymbolNavigationAction extends EditorAction { if (highlight) { const modelNow = targetEditor.getModel(); - const ids = targetEditor.deltaDecorations([], [{ range, options: { className: 'symbolHighlight' } }]); + const ids = targetEditor.deltaDecorations([], [{ range, options: { description: 'symbol-navigate-action-highlight', className: 'symbolHighlight' } }]); setTimeout(() => { if (targetEditor.getModel() === modelNow) { targetEditor.deltaDecorations(ids, []); diff --git a/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.css b/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.css index bb1fc6f543..412326ab89 100644 --- a/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.css +++ b/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.css @@ -6,4 +6,4 @@ .monaco-editor .goto-definition-link { text-decoration: underline; cursor: pointer; -} \ No newline at end of file +} diff --git a/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts b/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts index 348e3c8209..34acfc8c6f 100644 --- a/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts +++ b/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts @@ -305,6 +305,7 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri const newDecorations: IModelDeltaDecoration = { range: range, options: { + description: 'goto-definition-link', inlineClassName: 'goto-definition-link', hoverMessage } diff --git a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts index a302389e2a..666f411418 100644 --- a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts +++ b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts @@ -40,6 +40,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; class DecorationsManager implements IDisposable { private static readonly DecorationOptions = ModelDecorationOptions.register({ + description: 'reference-decoration', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'reference-decoration' }); diff --git a/src/vs/editor/contrib/hover/colorHoverParticipant.ts b/src/vs/editor/contrib/hover/colorHoverParticipant.ts new file mode 100644 index 0000000000..b53c3ccd76 --- /dev/null +++ b/src/vs/editor/contrib/hover/colorHoverParticipant.ts @@ -0,0 +1,153 @@ +/*--------------------------------------------------------------------------------------------- + * 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, IDisposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import { DocumentColorProvider, IColorInformation } from 'vs/editor/common/modes'; +import { IIdentifiedSingleEditOperation, IModelDecoration, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { HoverAnchor, HoverAnchorType, IEditorHover, IEditorHoverParticipant, IEditorHoverStatusBar, IHoverPart } from 'vs/editor/contrib/hover/hoverTypes'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { getColorPresentations } from 'vs/editor/contrib/colorPicker/color'; +import { ColorDetector } from 'vs/editor/contrib/colorPicker/colorDetector'; +import { Color, RGBA } from 'vs/base/common/color'; +import { ColorPickerModel } from 'vs/editor/contrib/colorPicker/colorPickerModel'; +import { ColorPickerWidget } from 'vs/editor/contrib/colorPicker/colorPickerWidget'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; + +export class ColorHover implements IHoverPart { + + /** + * Force the hover to always be rendered at this specific range, + * even in the case of multiple hover parts. + */ + public readonly forceShowAtRange: boolean = true; + + constructor( + public readonly owner: IEditorHoverParticipant, + public readonly range: Range, + public readonly model: ColorPickerModel, + public readonly provider: DocumentColorProvider + ) { } + + public isValidForHoverAnchor(anchor: HoverAnchor): boolean { + return ( + anchor.type === HoverAnchorType.Range + && this.range.startColumn <= anchor.range.startColumn + && this.range.endColumn >= anchor.range.endColumn + ); + } +} + +export class ColorHoverParticipant implements IEditorHoverParticipant { + + constructor( + private readonly _editor: ICodeEditor, + private readonly _hover: IEditorHover, + @IThemeService private readonly _themeService: IThemeService, + ) { } + + public computeSync(anchor: HoverAnchor, lineDecorations: IModelDecoration[]): ColorHover[] { + return []; + } + + public async computeAsync(anchor: HoverAnchor, lineDecorations: IModelDecoration[], token: CancellationToken): Promise { + if (!this._editor.hasModel()) { + return []; + } + const colorDetector = ColorDetector.get(this._editor); + for (const d of lineDecorations) { + const colorData = colorDetector.getColorData(d.range.getStartPosition()); + if (colorData) { + const colorHover = await this._createColorHover(this._editor.getModel(), colorData.colorInfo, colorData.provider); + return [colorHover]; + } + } + return []; + } + + private async _createColorHover(editorModel: ITextModel, colorInfo: IColorInformation, provider: DocumentColorProvider): Promise { + const originalText = editorModel.getValueInRange(colorInfo.range); + const { red, green, blue, alpha } = colorInfo.color; + const rgba = new RGBA(Math.round(red * 255), Math.round(green * 255), Math.round(blue * 255), alpha); + const color = new Color(rgba); + + const colorPresentations = await getColorPresentations(editorModel, colorInfo, provider, CancellationToken.None); + const model = new ColorPickerModel(color, [], 0); + model.colorPresentations = colorPresentations || []; + model.guessColorPresentation(color, originalText); + + return new ColorHover(this, Range.lift(colorInfo.range), model, provider); + } + + public renderHoverParts(hoverParts: ColorHover[], fragment: DocumentFragment, statusBar: IEditorHoverStatusBar): IDisposable { + if (hoverParts.length === 0 || !this._editor.hasModel()) { + return Disposable.None; + } + + const disposables = new DisposableStore(); + const colorHover = hoverParts[0]; + const editorModel = this._editor.getModel(); + const model = colorHover.model; + const widget = disposables.add(new ColorPickerWidget(fragment, model, this._editor.getOption(EditorOption.pixelRatio), this._themeService)); + + let range = new Range(colorHover.range.startLineNumber, colorHover.range.startColumn, colorHover.range.endLineNumber, colorHover.range.endColumn); + + const updateEditorModel = () => { + let textEdits: IIdentifiedSingleEditOperation[]; + let newRange: Range; + if (model.presentation.textEdit) { + textEdits = [model.presentation.textEdit as IIdentifiedSingleEditOperation]; + newRange = new Range( + model.presentation.textEdit.range.startLineNumber, + model.presentation.textEdit.range.startColumn, + model.presentation.textEdit.range.endLineNumber, + model.presentation.textEdit.range.endColumn + ); + const trackedRange = this._editor.getModel()!._setTrackedRange(null, newRange, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter); + this._editor.pushUndoStop(); + this._editor.executeEdits('colorpicker', textEdits); + newRange = this._editor.getModel()!._getTrackedRange(trackedRange) || newRange; + } else { + textEdits = [{ identifier: null, range, text: model.presentation.label, forceMoveMarkers: false }]; + newRange = range.setEndPosition(range.endLineNumber, range.startColumn + model.presentation.label.length); + this._editor.pushUndoStop(); + this._editor.executeEdits('colorpicker', textEdits); + } + + if (model.presentation.additionalTextEdits) { + textEdits = [...model.presentation.additionalTextEdits as IIdentifiedSingleEditOperation[]]; + this._editor.executeEdits('colorpicker', textEdits); + this._hover.hide(); + } + this._editor.pushUndoStop(); + range = newRange; + }; + + const updateColorPresentations = (color: Color) => { + return getColorPresentations(editorModel, { + range: range, + color: { + red: color.rgba.r / 255, + green: color.rgba.g / 255, + blue: color.rgba.b / 255, + alpha: color.rgba.a + } + }, colorHover.provider, CancellationToken.None).then((colorPresentations) => { + model.colorPresentations = colorPresentations || []; + }); + }; + + disposables.add(model.onColorFlushed((color: Color) => { + updateColorPresentations(color).then(updateEditorModel); + })); + disposables.add(model.onDidChangeColor(updateColorPresentations)); + + this._hover.setColorPicker(widget); + + return disposables; + } +} diff --git a/src/vs/editor/contrib/hover/hover.ts b/src/vs/editor/contrib/hover/hover.ts index f89d377238..49b134808c 100644 --- a/src/vs/editor/contrib/hover/hover.ts +++ b/src/vs/editor/contrib/hover/hover.ts @@ -7,7 +7,6 @@ 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 } 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'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -20,8 +19,8 @@ import { ModesContentHoverWidget } from 'vs/editor/contrib/hover/modesContentHov import { ModesGlyphHoverWidget } from 'vs/editor/contrib/hover/modesGlyphHover'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; 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 { editorHoverBackground, editorHoverBorder, editorHoverHighlight, textCodeBlockBackground, textLinkForeground, editorHoverStatusBarBackground, editorHoverForeground, textLinkActiveForeground } from 'vs/platform/theme/common/colorRegistry'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; 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'; @@ -52,7 +51,6 @@ export class ModesHoverController implements IEditorContribution { @IInstantiationService private readonly _instantiationService: IInstantiationService, @IOpenerService private readonly _openerService: IOpenerService, @IModeService private readonly _modeService: IModeService, - @IThemeService private readonly _themeService: IThemeService, @IContextKeyService _contextKeyService: IContextKeyService ) { this._isMouseDown = false; @@ -166,45 +164,27 @@ export class ModesHoverController implements IEditorContribution { return; } - if (targetType === MouseTargetType.CONTENT_EMPTY) { - const epsilon = this._editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth / 2; - const data = mouseEvent.target.detail; - if (data && !data.isAfterLines && typeof data.horizontalDistanceToText === 'number' && data.horizontalDistanceToText < epsilon) { - // Let hover kick in even when the mouse is technically in the empty area after a line, given the distance is small enough - targetType = MouseTargetType.CONTENT_TEXT; - } - } - - if (targetType === MouseTargetType.CONTENT_TEXT) { - 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; - 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(); - - if (this._isHoverEnabled && mouseEvent.target.position) { - if (!this._glyphWidget) { - this._glyphWidget = new ModesGlyphHoverWidget(this._editor, this._modeService, this._openerService); - } - this._glyphWidget.startShowingAt(mouseEvent.target.position.lineNumber); - } - } else { + if (!this._isHoverEnabled) { this._hideWidgets(); + return; } + + const contentWidget = this._getOrCreateContentWidget(); + if (contentWidget.maybeShowAt(mouseEvent)) { + this._glyphWidget?.hide(); + return; + } + + if (targetType === MouseTargetType.GUTTER_GLYPH_MARGIN && mouseEvent.target.position) { + this._contentWidget?.hide(); + if (!this._glyphWidget) { + this._glyphWidget = new ModesGlyphHoverWidget(this._editor, this._modeService, this._openerService); + } + this._glyphWidget.startShowingAt(mouseEvent.target.position.lineNumber); + return; + } + + this._hideWidgets(); } private _onKeyDown(e: IKeyboardEvent): void { @@ -224,15 +204,19 @@ export class ModesHoverController implements IEditorContribution { this._contentWidget?.hide(); } + private _getOrCreateContentWidget(): ModesContentHoverWidget { + if (!this._contentWidget) { + this._contentWidget = this._instantiationService.createInstance(ModesContentHoverWidget, this._editor, this._hoverVisibleKey); + } + return this._contentWidget; + } + public isColorPickerVisible(): boolean { return this._contentWidget?.isColorPickerVisible() || false; } public showContentHover(range: Range, mode: HoverStartMode, focus: boolean): void { - if (!this._contentWidget) { - this._contentWidget = new ModesContentHoverWidget(this._editor, this._hoverVisibleKey, this._instantiationService, this._themeService); - } - this._contentWidget.startShowingAt(range, mode, focus); + this._getOrCreateContentWidget().startShowingAtRange(range, mode, focus); } public dispose(): void { @@ -343,6 +327,10 @@ registerThemingParticipant((theme, collector) => { if (link) { collector.addRule(`.monaco-editor .monaco-hover a { color: ${link}; }`); } + const linkHover = theme.getColor(textLinkActiveForeground); + if (linkHover) { + collector.addRule(`.monaco-editor .monaco-hover a:hover { color: ${linkHover}; }`); + } const hoverForeground = theme.getColor(editorHoverForeground); if (hoverForeground) { collector.addRule(`.monaco-editor .monaco-hover { color: ${hoverForeground}; }`); diff --git a/src/vs/editor/contrib/hover/hoverTypes.ts b/src/vs/editor/contrib/hover/hoverTypes.ts new file mode 100644 index 0000000000..65beed6752 --- /dev/null +++ b/src/vs/editor/contrib/hover/hoverTypes.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { ColorPickerWidget } from 'vs/editor/contrib/colorPicker/colorPickerWidget'; +import { IModelDecoration } from 'vs/editor/common/model'; + +export interface IHoverPart { + /** + * The creator of this hover part. + */ + readonly owner: IEditorHoverParticipant; + /** + * The range where this hover part applies. + */ + readonly range: Range; + /** + * Force the hover to always be rendered at this specific range, + * even in the case of multiple hover parts. + */ + readonly forceShowAtRange?: boolean; + + isValidForHoverAnchor(anchor: HoverAnchor): boolean; +} + +export interface IEditorHover { + hide(): void; + onContentsChanged(): void; + setColorPicker(widget: ColorPickerWidget): void; +} + +export const enum HoverAnchorType { + Range = 1, + ForeignElement = 2 +} + +export class HoverRangeAnchor { + public readonly type = HoverAnchorType.Range; + constructor( + public readonly priority: number, + public readonly range: Range + ) { + } + public equals(other: HoverAnchor) { + return (other.type === HoverAnchorType.Range && this.range.equalsRange(other.range)); + } + public canAdoptVisibleHover(lastAnchor: HoverAnchor, showAtPosition: Position): boolean { + return (lastAnchor.type === HoverAnchorType.Range && showAtPosition.lineNumber === this.range.startLineNumber); + } +} + +export class HoverForeignElementAnchor { + public readonly type = HoverAnchorType.ForeignElement; + constructor( + public readonly priority: number, + public readonly owner: IEditorHoverParticipant, + public readonly range: Range + ) { + } + public equals(other: HoverAnchor) { + return (other.type === HoverAnchorType.ForeignElement && this.owner === other.owner); + } + public canAdoptVisibleHover(lastAnchor: HoverAnchor, showAtPosition: Position): boolean { + return (lastAnchor.type === HoverAnchorType.ForeignElement && this.owner === lastAnchor.owner); + } +} + +export type HoverAnchor = HoverRangeAnchor | HoverForeignElementAnchor; + +export interface IEditorHoverStatusBar { + addAction(actionOptions: { label: string, iconClass?: string, run: (target: HTMLElement) => void, commandId: string }): void; + append(element: HTMLElement): HTMLElement; +} + +export interface IEditorHoverParticipant { + suggestHoverAnchor?(mouseEvent: IEditorMouseEvent): HoverAnchor | null; + computeSync(anchor: HoverAnchor, lineDecorations: IModelDecoration[]): T[]; + computeAsync?(anchor: HoverAnchor, lineDecorations: IModelDecoration[], token: CancellationToken): Promise; + createLoadingMessage?(anchor: HoverAnchor): T | null; + renderHoverParts(hoverParts: T[], fragment: DocumentFragment, statusBar: IEditorHoverStatusBar): IDisposable; +} diff --git a/src/vs/editor/contrib/hover/markdownHoverParticipant.ts b/src/vs/editor/contrib/hover/markdownHoverParticipant.ts index 337646f023..7f9b8d4587 100644 --- a/src/vs/editor/contrib/hover/markdownHoverParticipant.ts +++ b/src/vs/editor/contrib/hover/markdownHoverParticipant.ts @@ -5,7 +5,7 @@ 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 { IMarkdownString, MarkdownString, isEmptyMarkdownString } 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'; @@ -14,26 +14,29 @@ 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 { HoverAnchor, HoverAnchorType, IEditorHover, IEditorHoverParticipant, IEditorHoverStatusBar, IHoverPart } from 'vs/editor/contrib/hover/hoverTypes'; 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'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; const $ = dom.$; export class MarkdownHover implements IHoverPart { constructor( + public readonly owner: IEditorHoverParticipant, 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; + public isValidForHoverAnchor(anchor: HoverAnchor): boolean { + return ( + anchor.type === HoverAnchorType.Range + && this.range.startColumn <= anchor.range.startColumn + && this.range.endColumn >= anchor.range.endColumn + ); } } @@ -44,19 +47,20 @@ export class MarkdownHoverParticipant implements IEditorHoverParticipant('editor.maxTokenizationLineLength'); + if (lineLength >= maxTokenizationLineLength) { + result.push(new MarkdownHover(this, new Range(lineNumber, 1, lineNumber, lineLength + 1), [{ + value: nls.localize('too many characters', "Tokenization is skipped for long lines for performance reasons. This can be configured via `editor.maxTokenizationLineLength`.") + }])); } return result; } - public async computeAsync(range: Range, token: CancellationToken): Promise { - if (!this._editor.hasModel() || !range) { + public async computeAsync(anchor: HoverAnchor, lineDecorations: IModelDecoration[], token: CancellationToken): Promise { + if (!this._editor.hasModel() || anchor.type !== HoverAnchorType.Range) { return Promise.resolve([]); } @@ -87,8 +99,8 @@ export class MarkdownHoverParticipant implements IEditorHoverParticipant, 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; + public isValidForHoverAnchor(anchor: HoverAnchor): boolean { + return ( + anchor.type === HoverAnchorType.Range + && this.range.startColumn <= anchor.range.startColumn + && this.range.endColumn >= anchor.range.endColumn + ); } } @@ -58,17 +58,16 @@ export class MarkerHoverParticipant implements IEditorHoverParticipant 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)); + this.renderMarkerStatusbar(markerHoverForStatusbar, statusBar, disposables); return disposables; } @@ -165,11 +164,9 @@ export class MarkerHoverParticipant implements IEditorHoverParticipant { @@ -177,11 +174,11 @@ export class MarkerHoverParticipant implements IEditorHoverParticipant { @@ -231,17 +228,9 @@ export class MarkerHoverParticipant implements IEditorHoverParticipant 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 { diff --git a/src/vs/editor/contrib/hover/modesContentHover.ts b/src/vs/editor/contrib/hover/modesContentHover.ts index a80a6dd8e1..01f126e33f 100644 --- a/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/modesContentHover.ts @@ -5,21 +5,17 @@ import * as dom from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { Color, RGBA } from 'vs/base/common/color'; -import { IDisposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle'; -import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; +import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidget, IContentWidgetPosition, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -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 { TokenizationRegistry } from 'vs/editor/common/modes'; import { ColorPickerWidget } from 'vs/editor/contrib/colorPicker/colorPickerWidget'; import { HoverOperation, HoverStartMode, IHoverComputer } from 'vs/editor/contrib/hover/hoverOperation'; -import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { coalesce } from 'vs/base/common/arrays'; -import { IIdentifiedSingleEditOperation, IModelDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { coalesce, flatten } from 'vs/base/common/arrays'; +import { IModelDecoration } 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'; @@ -27,65 +23,67 @@ 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 { HoverWidget, renderHoverAction } from 'vs/base/browser/ui/hover/hoverWidget'; +import { MarkerHoverParticipant } from 'vs/editor/contrib/hover/markerHoverParticipant'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { MarkdownHover, MarkdownHoverParticipant } from 'vs/editor/contrib/hover/markdownHoverParticipant'; +import { MarkdownHoverParticipant } from 'vs/editor/contrib/hover/markdownHoverParticipant'; +import { InlineCompletionsHoverParticipant } from 'vs/editor/contrib/inlineCompletions/inlineCompletionsHoverParticipant'; +import { ColorHoverParticipant } from 'vs/editor/contrib/hover/colorHoverParticipant'; +import { IEmptyContentData } from 'vs/editor/browser/controller/mouseTarget'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IEditorHoverStatusBar, IHoverPart, HoverAnchor, IEditorHoverParticipant, HoverAnchorType, IEditorHover, HoverRangeAnchor } from 'vs/editor/contrib/hover/hoverTypes'; -export interface IHoverPart { - readonly range: Range; - equals(other: IHoverPart): boolean; -} +const $ = dom.$; -export interface IEditorHover { - hide(): void; - onContentsChanged(): void; -} +class EditorHoverStatusBar extends Disposable implements IEditorHoverStatusBar { -export interface IEditorHoverParticipant { - computeSync(hoverRange: Range, lineDecorations: IModelDecoration[]): T[]; - computeAsync?(range: Range, token: CancellationToken): Promise; - renderHoverParts(hoverParts: T[], fragment: DocumentFragment): IDisposable; -} + public readonly hoverElement: HTMLElement; + private readonly actionsElement: HTMLElement; + private _hasContent: boolean = false; -class ColorHover implements IHoverPart { + public get hasContent() { + return this._hasContent; + } constructor( - public readonly range: Range, - public readonly color: IColor, - public readonly provider: DocumentColorProvider - ) { } + @IKeybindingService private readonly _keybindingService: IKeybindingService, + ) { + super(); + this.hoverElement = $('div.hover-row.status-bar'); + this.actionsElement = dom.append(this.hoverElement, $('div.actions')); + } - equals(other: IHoverPart): boolean { - return false; + public addAction(actionOptions: { label: string, iconClass?: string, run: (target: HTMLElement) => void, commandId: string }): void { + const keybinding = this._keybindingService.lookupKeybinding(actionOptions.commandId); + const keybindingLabel = keybinding ? keybinding.getLabel() : null; + this._register(renderHoverAction(this.actionsElement, actionOptions, keybindingLabel)); + this._hasContent = true; + } + + public append(element: HTMLElement): HTMLElement { + const result = dom.append(this.actionsElement, element); + this._hasContent = true; + return result; } } -class HoverPartInfo { - constructor( - public readonly owner: IEditorHoverParticipant | null, - public readonly data: IHoverPart - ) { } -} - -class ModesContentComputer implements IHoverComputer { +class ModesContentComputer implements IHoverComputer { private readonly _editor: ICodeEditor; - private _result: HoverPartInfo[]; - private _range: Range | null; + private _result: IHoverPart[]; + private _anchor: HoverAnchor | null; constructor( editor: ICodeEditor, - private readonly _markerHoverParticipant: IEditorHoverParticipant, - private readonly _markdownHoverParticipant: MarkdownHoverParticipant + private readonly _participants: readonly IEditorHoverParticipant[] ) { this._editor = editor; this._result = []; - this._range = null; + this._anchor = null; } - public setRange(range: Range): void { - this._range = range; + public setAnchor(anchor: HoverAnchor): void { + this._anchor = anchor; this._result = []; } @@ -93,65 +91,64 @@ class ModesContentComputer implements IHoverComputer { this._result = []; } - public async computeAsync(token: CancellationToken): Promise { - if (!this._editor.hasModel() || !this._range) { - return Promise.resolve([]); - } - - const markdownHovers = await this._markdownHoverParticipant.computeAsync(this._range, token); - return markdownHovers.map(h => new HoverPartInfo(this._markdownHoverParticipant, h)); - } - - public computeSync(): HoverPartInfo[] { - if (!this._editor.hasModel() || !this._range) { - return []; - } - - const model = this._editor.getModel(); - const hoverRange = this._range; - const lineNumber = hoverRange.startLineNumber; - - if (lineNumber > this._editor.getModel().getLineCount()) { - // Illegal line number => no results + private static _getLineDecorations(editor: IActiveCodeEditor, anchor: HoverAnchor): IModelDecoration[] { + if (anchor.type !== HoverAnchorType.Range) { return []; } + const model = editor.getModel(); + const lineNumber = anchor.range.startLineNumber; const maxColumn = model.getLineMaxColumn(lineNumber); - const lineDecorations = this._editor.getLineDecorations(lineNumber).filter((d) => { + return editor.getLineDecorations(lineNumber).filter((d) => { if (d.options.isWholeLine) { return true; } const startColumn = (d.range.startLineNumber === lineNumber) ? d.range.startColumn : 1; const endColumn = (d.range.endLineNumber === lineNumber) ? d.range.endColumn : maxColumn; - if (startColumn > hoverRange.startColumn || hoverRange.endColumn > endColumn) { + if (startColumn > anchor.range.startColumn || anchor.range.endColumn > endColumn) { return false; } return true; }); + } - let result: HoverPartInfo[] = []; + public async computeAsync(token: CancellationToken): Promise { + const anchor = this._anchor; - 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; - } + if (!this._editor.hasModel() || !anchor) { + return Promise.resolve([]); } - const markdownHovers = this._markdownHoverParticipant.computeSync(this._range, lineDecorations); - result = result.concat(markdownHovers.map(h => new HoverPartInfo(this._markdownHoverParticipant, h))); + const lineDecorations = ModesContentComputer._getLineDecorations(this._editor, anchor); - const markerHovers = this._markerHoverParticipant.computeSync(this._range, lineDecorations); - result = result.concat(markerHovers.map(h => new HoverPartInfo(this._markerHoverParticipant, h))); + const allResults = await Promise.all(this._participants.map(p => this._computeAsync(p, lineDecorations, anchor, token))); + return flatten(allResults); + } + + private async _computeAsync(participant: IEditorHoverParticipant, lineDecorations: IModelDecoration[], anchor: HoverAnchor, token: CancellationToken): Promise { + if (!participant.computeAsync) { + return []; + } + return participant.computeAsync(anchor, lineDecorations, token); + } + + public computeSync(): IHoverPart[] { + if (!this._editor.hasModel() || !this._anchor) { + return []; + } + + const lineDecorations = ModesContentComputer._getLineDecorations(this._editor, this._anchor); + + let result: IHoverPart[] = []; + for (const participant of this._participants) { + result = result.concat(participant.computeSync(this._anchor, lineDecorations)); + } return coalesce(result); } - public onResult(result: HoverPartInfo[], isFromSynchronousComputation: boolean): void { + public onResult(result: IHoverPart[], isFromSynchronousComputation: boolean): void { // Always put synchronous messages before asynchronous ones if (isFromSynchronousComputation) { this._result = result.concat(this._result); @@ -160,15 +157,20 @@ class ModesContentComputer implements IHoverComputer { } } - public getResult(): HoverPartInfo[] { + public getResult(): IHoverPart[] { return this._result.slice(0); } - public getResultWithLoadingMessage(): HoverPartInfo[] { - if (this._range) { - const loadingMessage = new HoverPartInfo(this._markdownHoverParticipant, this._markdownHoverParticipant.createLoadingMessage(this._range)); - return this._result.slice(0).concat([loadingMessage]); - + public getResultWithLoadingMessage(): IHoverPart[] { + if (this._anchor) { + for (const participant of this._participants) { + if (participant.createLoadingMessage) { + const loadingMessage = participant.createLoadingMessage(this._anchor); + if (loadingMessage) { + return this._result.slice(0).concat([loadingMessage]); + } + } + } } return this._result.slice(0); } @@ -178,8 +180,7 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I static readonly ID = 'editor.contrib.modesContentHoverWidget'; - private readonly _markerHoverParticipant: IEditorHoverParticipant; - private readonly _markdownHoverParticipant: MarkdownHoverParticipant; + private readonly _participants: IEditorHoverParticipant[]; private readonly _hover: HoverWidget; private readonly _id: string; @@ -192,10 +193,10 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I // IContentWidget.allowEditorOverflow public readonly allowEditorOverflow = true; - private _messages: HoverPartInfo[]; - private _lastRange: Range | null; + private _messages: IHoverPart[]; + private _lastAnchor: HoverAnchor | null; private readonly _computer: ModesContentComputer; - private readonly _hoverOperation: HoverOperation; + private readonly _hoverOperation: HoverOperation; private _highlightDecorations: string[]; private _isChangingDecorations: boolean; private _shouldFocus: boolean; @@ -205,13 +206,17 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I constructor( editor: ICodeEditor, private readonly _hoverVisibleKey: IContextKey, - instantiationService: IInstantiationService, - private readonly _themeService: IThemeService, + @IInstantiationService instantiationService: IInstantiationService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, ) { super(); - this._markerHoverParticipant = instantiationService.createInstance(MarkerHoverParticipant, editor, this); - this._markdownHoverParticipant = instantiationService.createInstance(MarkdownHoverParticipant, editor, this); + this._participants = [ + instantiationService.createInstance(ColorHoverParticipant, editor, this), + instantiationService.createInstance(MarkdownHoverParticipant, editor, this), + instantiationService.createInstance(InlineCompletionsHoverParticipant, editor, this), + instantiationService.createInstance(MarkerHoverParticipant, editor, this), + ]; this._hover = this._register(new HoverWidget()); this._id = ModesContentHoverWidget.ID; @@ -241,8 +246,8 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I this._stoleFocus = false; this._messages = []; - this._lastRange = null; - this._computer = new ModesContentComputer(this._editor, this._markerHoverParticipant, this._markdownHoverParticipant); + this._lastAnchor = null; + this._computer = new ModesContentComputer(this._editor, this._participants); this._highlightDecorations = []; this._isChangingDecorations = false; this._shouldFocus = false; @@ -268,26 +273,9 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I this._hoverOperation.setHoverTime(this._editor.getOption(EditorOption.hover).delay); })); 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.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, - green: color.rgba.g / 255, - blue: color.rgba.b / 255, - alpha: color.rgba.a - }; - return new HoverPartInfo(msg.owner, new ColorHover(msg.data.range, newColor, msg.data.provider)); - } else { - return msg; - } - }); - + if (this._isVisible && this._lastAnchor && this._messages.length > 0) { this._hover.contentsDomNode.textContent = ''; - this._renderMessages(this._lastRange, this._messages); + this._renderMessages(this._lastAnchor, this._messages); } })); } @@ -306,7 +294,60 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I return this._hover.containerDomNode; } - public showAt(position: Position, range: Range | null, focus: boolean): void { + private _shouldShowAt(mouseEvent: IEditorMouseEvent): boolean { + const targetType = mouseEvent.target.type; + if (targetType === MouseTargetType.CONTENT_TEXT) { + return true; + } + + if (targetType === MouseTargetType.CONTENT_EMPTY) { + const epsilon = this._editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth / 2; + const data = mouseEvent.target.detail; + if (data && !data.isAfterLines && typeof data.horizontalDistanceToText === 'number' && data.horizontalDistanceToText < epsilon) { + // Let hover kick in even when the mouse is technically in the empty area after a line, given the distance is small enough + return true; + } + } + + return false; + } + + public maybeShowAt(mouseEvent: IEditorMouseEvent): boolean { + const anchorCandidates: HoverAnchor[] = []; + + for (const participant of this._participants) { + if (typeof participant.suggestHoverAnchor === 'function') { + const anchor = participant.suggestHoverAnchor(mouseEvent); + if (anchor) { + anchorCandidates.push(anchor); + } + } + } + + if (this._shouldShowAt(mouseEvent) && 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; + 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 + ); + anchorCandidates.push(new HoverRangeAnchor(0, showAtRange)); + } + + if (anchorCandidates.length === 0) { + return false; + } + + anchorCandidates.sort((a, b) => b.priority - a.priority); + this._startShowingAt(anchorCandidates[0], HoverStartMode.Delayed, false); + + return true; + } + + private _showAt(position: Position, range: Range | null, focus: boolean): void { // Position has changed this._showAtPosition = position; this._showAtRange = range; @@ -378,8 +419,12 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I } } - public startShowingAt(range: Range, mode: HoverStartMode, focus: boolean): void { - if (this._lastRange && this._lastRange.equalsRange(range)) { + public startShowingAtRange(range: Range, mode: HoverStartMode, focus: boolean): void { + this._startShowingAt(new HoverRangeAnchor(0, range), mode, focus); + } + + private _startShowingAt(anchor: HoverAnchor, mode: HoverStartMode, focus: boolean): void { + if (this._lastAnchor && this._lastAnchor.equals(anchor)) { // We have to show the widget at the exact same range as before, so no work is needed return; } @@ -390,36 +435,29 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I // 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) { + if (!this._showAtPosition || !this._lastAnchor || !anchor.canAdoptVisibleHover(this._lastAnchor, this._showAtPosition)) { this.hide(); } else { - let filteredMessages: HoverPartInfo[] = []; - for (let i = 0, len = this._messages.length; i < len; i++) { - const msg = this._messages[i]; - const rng = msg.data.range; - if (rng && rng.startColumn <= range.startColumn && rng.endColumn >= range.endColumn) { - filteredMessages.push(msg); - } - } - if (filteredMessages.length > 0) { - if (hoverContentsEquals(filteredMessages, this._messages)) { - return; - } - this._renderMessages(range, filteredMessages); - } else { + const filteredMessages = this._messages.filter((m) => m.isValidForHoverAnchor(anchor)); + if (filteredMessages.length === 0) { this.hide(); + } else if (filteredMessages.length === this._messages.length) { + // no change + return; + } else { + this._renderMessages(anchor, filteredMessages); } } } - this._lastRange = range; - this._computer.setRange(range); + this._lastAnchor = anchor; + this._computer.setAnchor(anchor); this._shouldFocus = focus; this._hoverOperation.start(mode); } public hide(): void { - this._lastRange = null; + this._lastAnchor = null; this._hoverOperation.cancel(); if (this._isVisible) { @@ -452,157 +490,79 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I return !!this._colorPicker; } + public setColorPicker(widget: ColorPickerWidget): void { + this._colorPicker = widget; + } + public onContentsChanged(): void { this._hover.onContentsChanged(); } - private _withResult(result: HoverPartInfo[], complete: boolean): void { + private _withResult(result: IHoverPart[], complete: boolean): void { this._messages = result; - if (this._lastRange && this._messages.length > 0) { - this._renderMessages(this._lastRange, this._messages); + if (this._lastAnchor && this._messages.length > 0) { + this._renderMessages(this._lastAnchor, this._messages); } else if (complete) { this.hide(); } } - private _renderMessages(renderRange: Range, messages: HoverPartInfo[]): void { + private _renderMessages(anchor: HoverAnchor, messages: IHoverPart[]): void { if (this._renderDisposable) { this._renderDisposable.dispose(); this._renderDisposable = null; } - this._colorPicker = null; + this._colorPicker = null as ColorPickerWidget | null; // TODO: TypeScript thinks this is always null // update column from which to show let renderColumn = Constants.MAX_SAFE_SMALL_INTEGER; - let highlightRange: Range | null = messages[0].data.range ? Range.lift(messages[0].data.range) : null; + let highlightRange: Range = messages[0].range; + let forceShowAtRange: Range | null = null; let fragment = document.createDocumentFragment(); - let containColorPicker = false; const disposables = new DisposableStore(); - const markerMessages: MarkerHover[] = []; - const markdownParts: MarkdownHover[] = []; - messages.forEach((_msg) => { - const msg = _msg.data; - if (!msg.range) { - return; - } - + const hoverParts = new Map(); + for (const msg of messages) { renderColumn = Math.min(renderColumn, msg.range.startColumn); - highlightRange = highlightRange ? Range.plusRange(highlightRange, msg.range) : Range.lift(msg.range); + highlightRange = Range.plusRange(highlightRange, msg.range); - if (msg instanceof ColorHover) { - containColorPicker = true; - - const { red, green, blue, alpha } = msg.color; - const rgba = new RGBA(Math.round(red * 255), Math.round(green * 255), Math.round(blue * 255), alpha); - const color = new Color(rgba); - - if (!this._editor.hasModel()) { - return; - } - - const editorModel = this._editor.getModel(); - let range = new Range(msg.range.startLineNumber, msg.range.startColumn, msg.range.endLineNumber, msg.range.endColumn); - let colorInfo = { range: msg.range, color: msg.color }; - - // create blank olor picker model and widget first to ensure it's positioned correctly. - const model = new ColorPickerModel(color, [], 0); - const widget = new ColorPickerWidget(fragment, model, this._editor.getOption(EditorOption.pixelRatio), this._themeService); - - getColorPresentations(editorModel, colorInfo, msg.provider, CancellationToken.None).then(colorPresentations => { - model.colorPresentations = colorPresentations || []; - if (!this._editor.hasModel()) { - // gone... - return; - } - const originalText = this._editor.getModel().getValueInRange(msg.range); - model.guessColorPresentation(color, originalText); - - const updateEditorModel = () => { - let textEdits: IIdentifiedSingleEditOperation[]; - let newRange: Range; - if (model.presentation.textEdit) { - textEdits = [model.presentation.textEdit as IIdentifiedSingleEditOperation]; - newRange = new Range( - model.presentation.textEdit.range.startLineNumber, - model.presentation.textEdit.range.startColumn, - model.presentation.textEdit.range.endLineNumber, - model.presentation.textEdit.range.endColumn - ); - const trackedRange = this._editor.getModel()!._setTrackedRange(null, newRange, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter); - this._editor.pushUndoStop(); - this._editor.executeEdits('colorpicker', textEdits); - newRange = this._editor.getModel()!._getTrackedRange(trackedRange) || newRange; - } else { - textEdits = [{ identifier: null, range, text: model.presentation.label, forceMoveMarkers: false }]; - newRange = range.setEndPosition(range.endLineNumber, range.startColumn + model.presentation.label.length); - this._editor.pushUndoStop(); - this._editor.executeEdits('colorpicker', textEdits); - } - - if (model.presentation.additionalTextEdits) { - textEdits = [...model.presentation.additionalTextEdits as IIdentifiedSingleEditOperation[]]; - this._editor.executeEdits('colorpicker', textEdits); - this.hide(); - } - this._editor.pushUndoStop(); - range = newRange; - }; - - const updateColorPresentations = (color: Color) => { - return getColorPresentations(editorModel, { - range: range, - color: { - red: color.rgba.r / 255, - green: color.rgba.g / 255, - blue: color.rgba.b / 255, - alpha: color.rgba.a - } - }, msg.provider, CancellationToken.None).then((colorPresentations) => { - model.colorPresentations = colorPresentations || []; - }); - }; - - const colorListener = model.onColorFlushed((color: Color) => { - updateColorPresentations(color).then(updateEditorModel); - }); - const colorChangeListener = model.onDidChangeColor(updateColorPresentations); - - this._colorPicker = widget; - this.showAt(range.getStartPosition(), range, this._shouldFocus); - this._updateContents(fragment); - this._colorPicker.layout(); - - this._renderDisposable = combinedDisposable(colorListener, colorChangeListener, widget, disposables); - }); - } else { - if (msg instanceof MarkerHover) { - markerMessages.push(msg); - } else { - if (msg instanceof MarkdownHover) { - markdownParts.push(msg); - } - } + if (msg.forceShowAtRange) { + forceShowAtRange = msg.range; } - }); - if (markdownParts.length > 0) { - disposables.add(this._markdownHoverParticipant.renderHoverParts(markdownParts, fragment)); + if (!hoverParts.has(msg.owner)) { + hoverParts.set(msg.owner, []); + } + const dest = hoverParts.get(msg.owner)!; + dest.push(msg); } - if (markerMessages.length) { - disposables.add(this._markerHoverParticipant.renderHoverParts(markerMessages, fragment)); + const statusBar = disposables.add(new EditorHoverStatusBar(this._keybindingService)); + + for (const [participant, participantHoverParts] of hoverParts) { + disposables.add(participant.renderHoverParts(participantHoverParts, fragment, statusBar)); + } + + if (statusBar.hasContent) { + fragment.appendChild(statusBar.hoverElement); } this._renderDisposable = disposables; // show - if (!containColorPicker && fragment.hasChildNodes()) { - this.showAt(new Position(renderRange.startLineNumber, renderColumn), highlightRange, this._shouldFocus); + if (fragment.hasChildNodes()) { + if (forceShowAtRange) { + this._showAt(forceShowAtRange.getStartPosition(), forceShowAtRange, this._shouldFocus); + } else { + this._showAt(new Position(anchor.range.startLineNumber, renderColumn), highlightRange, this._shouldFocus); + } this._updateContents(fragment); } + if (this._colorPicker) { + this._colorPicker.layout(); + } this._isChangingDecorations = true; this._highlightDecorations = this._editor.deltaDecorations(this._highlightDecorations, highlightRange ? [{ @@ -613,22 +573,11 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I } private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({ + description: 'content-hover-highlight', className: 'hoverHighlight' }); } -function hoverContentsEquals(first: HoverPartInfo[], second: HoverPartInfo[]): boolean { - if (first.length !== second.length) { - return false; - } - for (let i = 0; i < first.length; i++) { - if (!first[i].data.equals(second[i].data)) { - return false; - } - } - return true; -} - registerThemingParticipant((theme, collector) => { const linkFg = theme.getColor(textLinkForeground); if (linkFg) { diff --git a/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts b/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts index 20213213b1..da45d07a7f 100644 --- a/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts +++ b/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts @@ -31,6 +31,7 @@ class InPlaceReplaceController implements IEditorContribution { } private static readonly DECORATION = ModelDecorationOptions.register({ + description: 'in-place-replace', className: 'valueSetReplacement' }); diff --git a/src/vs/editor/contrib/indentation/indentUtils.ts b/src/vs/editor/contrib/indentation/indentUtils.ts index 4471c610db..292e1c4a65 100644 --- a/src/vs/editor/contrib/indentation/indentUtils.ts +++ b/src/vs/editor/contrib/indentation/indentUtils.ts @@ -34,4 +34,4 @@ export function generateIndent(spacesCnt: number, tabSize: number, insertSpaces: } return result; -} \ No newline at end of file +} diff --git a/src/vs/editor/contrib/inlineHints/inlineHintsController.ts b/src/vs/editor/contrib/inlayHints/inlayHintsController.ts similarity index 68% rename from src/vs/editor/contrib/inlineHints/inlineHintsController.ts rename to src/vs/editor/contrib/inlayHints/inlayHintsController.ts index d9d09631b2..e3ec758a9f 100644 --- a/src/vs/editor/contrib/inlineHints/inlineHintsController.ts +++ b/src/vs/editor/contrib/inlayHints/inlayHintsController.ts @@ -12,32 +12,32 @@ 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 { InlayHintsProvider, InlayHintsProviderRegistry, InlayHint } 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 { editorInlayHintForeground, editorInlayHintBackground } 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'; +import { Position } from 'vs/editor/common/core/position'; const MAX_DECORATORS = 500; -export interface InlineHintsData { - list: InlineHint[]; - provider: InlineHintsProvider; +export interface InlayHintsData { + list: InlayHint[]; + provider: InlayHintsProvider; } -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 => { +export async function getInlayHints(model: ITextModel, ranges: Range[], token: CancellationToken): Promise { + const datas: InlayHintsData[] = []; + const providers = InlayHintsProviderRegistry.ordered(model).reverse(); + const promises = flatten(providers.map(provider => ranges.map(range => Promise.resolve(provider.provideInlayHints(model, range, token)).then(result => { if (result) { datas.push({ list: result, provider }); } @@ -50,17 +50,13 @@ export async function getInlineHints(model: ITextModel, ranges: Range[], token: return datas; } -export class InlineHintsController implements IEditorContribution { +export class InlayHintsController implements IEditorContribution { - static readonly ID: string = 'editor.contrib.InlineHints'; - - // static get(editor: ICodeEditor): InlineHintsController { - // return editor.getContribution(this.ID); - // } + static readonly ID: string = 'editor.contrib.InlayHints'; private readonly _disposables = new DisposableStore(); private readonly _sessionDisposables = new DisposableStore(); - private readonly _getInlineHintsDelays = new LanguageFeatureRequestDelays(InlineHintsProviderRegistry, 250, 2500); + private readonly _getInlayHintsDelays = new LanguageFeatureRequestDelays(InlayHintsProviderRegistry, 250, 2500); private _decorationsTypeIds: string[] = []; private _decorationIds: string[] = []; @@ -70,12 +66,12 @@ export class InlineHintsController implements IEditorContribution { @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @IThemeService private readonly _themeService: IThemeService, ) { - this._disposables.add(InlineHintsProviderRegistry.onDidChange(() => this._update())); + this._disposables.add(InlayHintsProviderRegistry.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)) { + if (e.hasChanged(EditorOption.inlayHints)) { this._update(); } })); @@ -92,13 +88,13 @@ export class InlineHintsController implements IEditorContribution { private _update(): void { this._sessionDisposables.clear(); - if (!this._editor.getOption(EditorOption.inlineHints).enabled) { + if (!this._editor.getOption(EditorOption.inlayHints).enabled) { this._removeAllDecorations(); return; } const model = this._editor.getModel(); - if (!model || !InlineHintsProviderRegistry.has(model)) { + if (!model || !InlayHintsProviderRegistry.has(model)) { this._removeAllDecorations(); return; } @@ -110,16 +106,16 @@ export class InlineHintsController implements IEditorContribution { this._sessionDisposables.add(toDisposable(() => cts.dispose(true))); const visibleRanges = this._editor.getVisibleRangesPlusViewportAboveBelow(); - const result = await getInlineHints(model, visibleRanges, cts.token); + const result = await getInlayHints(model, visibleRanges, cts.token); // update moving average - const newDelay = this._getInlineHintsDelays.update(model, Date.now() - t1); + const newDelay = this._getInlayHintsDelays.update(model, Date.now() - t1); scheduler.delay = newDelay; // render hints this._updateHintsDecorators(result); - }, this._getInlineHintsDelays.get(model)); + }, this._getInlayHintsDelays.get(model)); this._sessionDisposables.add(scheduler); @@ -131,28 +127,28 @@ export class InlineHintsController implements IEditorContribution { // 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())); + for (const provider of InlayHintsProviderRegistry.all(model)) { + if (typeof provider.onDidChangeInlayHints === 'function') { + providerListener.add(provider.onDidChangeInlayHints(() => scheduler.schedule())); } } } - private _updateHintsDecorators(hintsData: InlineHintsData[]): void { + private _updateHintsDecorators(hintsData: InlayHintsData[]): void { const { fontSize, fontFamily } = this._getLayoutInfo(); - const backgroundColor = this._themeService.getColorTheme().getColor(editorInlineHintBackground); - const fontColor = this._themeService.getColorTheme().getColor(editorInlineHintForeground); + const backgroundColor = this._themeService.getColorTheme().getColor(editorInlayHintBackground); + const fontColor = this._themeService.getColorTheme().getColor(editorInlayHintForeground); const newDecorationsTypeIds: string[] = []; const newDecorationsData: IModelDeltaDecoration[] = []; - const fontFamilyVar = '--inlineHintsFontFamily'; + const fontFamilyVar = '--inlayHintsFontFamily'; this._editor.getContainerDomNode().style.setProperty(fontFamilyVar, fontFamily); 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 { text, position, whitespaceBefore, whitespaceAfter } = hints[j]; const marginBefore = whitespaceBefore ? (fontSize / 3) | 0 : 0; const marginAfter = whitespaceAfter ? (fontSize / 3) | 0 : 0; @@ -166,22 +162,16 @@ export class InlineHintsController implements IEditorContribution { 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); + const key = 'inlayHints-' + hash(before).toString(16); + this._codeEditorService.registerDecorationType('inlay-hints-controller', 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, + range: Range.fromPositions(position), options }); } @@ -194,7 +184,7 @@ export class InlineHintsController implements IEditorContribution { } private _getLayoutInfo() { - const options = this._editor.getOption(EditorOption.inlineHints); + const options = this._editor.getOption(EditorOption.inlayHints); const editorFontSize = this._editor.getOption(EditorOption.fontSize); let fontSize = options.fontSize; if (!fontSize || fontSize < 5 || fontSize > editorFontSize) { @@ -211,9 +201,9 @@ export class InlineHintsController implements IEditorContribution { } } -registerEditorContribution(InlineHintsController.ID, InlineHintsController); +registerEditorContribution(InlayHintsController.ID, InlayHintsController); -CommandsRegistry.registerCommand('_executeInlineHintProvider', async (accessor, ...args: [URI, IRange]): Promise => { +CommandsRegistry.registerCommand('_executeInlayHintProvider', async (accessor, ...args: [URI, IRange]): Promise => { const [uri, range] = args; assertType(URI.isUri(uri)); @@ -221,8 +211,8 @@ CommandsRegistry.registerCommand('_executeInlineHintProvider', async (accessor, 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)); + const data = await getInlayHints(ref.object.textEditorModel, [Range.lift(range)], CancellationToken.None); + return flatten(data.map(item => item.list)).sort((a, b) => Position.compare(a.position, b.position)); } finally { ref.dispose(); diff --git a/src/vs/editor/contrib/inlineCompletions/ghostText.css b/src/vs/editor/contrib/inlineCompletions/ghostText.css new file mode 100644 index 0000000000..98ec32860a --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/ghostText.css @@ -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. + *--------------------------------------------------------------------------------------------*/ + +.monaco-editor .suggest-preview-additional-widget { + white-space: nowrap; +} + +.monaco-editor .suggest-preview-additional-widget .content-spacer { + color: transparent; + white-space: pre; +} + +.monaco-editor .suggest-preview-additional-widget .button { + display: inline-block; + cursor: pointer; + text-decoration: underline; + text-underline-position: under; +} diff --git a/src/vs/editor/contrib/inlineCompletions/ghostTextController.ts b/src/vs/editor/contrib/inlineCompletions/ghostTextController.ts new file mode 100644 index 0000000000..afeba9d417 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/ghostTextController.ts @@ -0,0 +1,343 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { Range } from 'vs/editor/common/core/range'; +import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, EditorCommand, registerEditorAction, registerEditorCommand, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { GhostTextWidget } from 'vs/editor/contrib/inlineCompletions/ghostTextWidget'; +import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/inlineCompletionsModel'; +import { SuggestWidgetAdapterModel } from 'vs/editor/contrib/inlineCompletions/suggestWidgetAdapterModel'; +import * as nls from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + +export class GhostTextController extends Disposable { + public static readonly inlineSuggestionVisible = new RawContextKey('inlineSuggestionVisible ', false, nls.localize('inlineSuggestionVisible', "Whether an inline suggestion is visible")); + public static readonly inlineSuggestionHasIndentation = new RawContextKey('inlineSuggestionHasIndentation', false, nls.localize('inlineSuggestionHasIndentation', "Whether the inline suggestion starts with whitespace")); + + static ID = 'editor.contrib.ghostTextController'; + + public static get(editor: ICodeEditor): GhostTextController { + return editor.getContribution(GhostTextController.ID); + } + + private readonly widget: GhostTextWidget; + private readonly activeController = this._register(new MutableDisposable()); + private readonly contextKeys: GhostTextContextKeys; + private triggeredExplicitly = false; + + constructor( + public readonly editor: ICodeEditor, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + super(); + this.contextKeys = new GhostTextContextKeys(contextKeyService); + + this.widget = this._register(instantiationService.createInstance(GhostTextWidget, this.editor)); + + this._register(this.editor.onDidChangeModel(() => { + this.updateModelController(); + })); + this._register(this.editor.onDidChangeConfiguration((e) => { + if (e.hasChanged(EditorOption.suggest)) { + this.updateModelController(); + } + })); + this.updateModelController(); + } + + // Don't call this method when not neccessary. It will recreate the activeController. + private updateModelController(): void { + const suggestOptions = this.editor.getOption(EditorOption.suggest); + const inlineSuggestOptions = this.editor.getOption(EditorOption.inlineSuggest); + + this.activeController.value = undefined; + // ActiveGhostTextController is only created if one of those settings is set or if the inline completions are triggered explicitly. + this.activeController.value = + this.editor.hasModel() && (suggestOptions.preview || inlineSuggestOptions.enabled || this.triggeredExplicitly) + ? this.instantiationService.createInstance( + ActiveGhostTextController, + this.editor, + this.widget, + this.contextKeys + ) + : undefined; + } + + public shouldShowHoverAt(hoverRange: Range): boolean { + return this.activeController.value?.shouldShowHoverAt(hoverRange) || false; + } + + public shouldShowHoverAtViewZone(viewZoneId: string): boolean { + return this.widget.shouldShowHoverAtViewZone(viewZoneId); + } + + public trigger(): void { + this.triggeredExplicitly = true; + if (!this.activeController.value) { + this.updateModelController(); + } + this.activeController.value?.triggerInlineCompletion(); + } + + public commit(): void { + this.activeController.value?.commitInlineCompletion(); + } + + public hide(): void { + this.activeController.value?.hideInlineCompletion(); + } + + public showNextInlineCompletion(): void { + this.activeController.value?.showNextInlineCompletion(); + } + + public showPreviousInlineCompletion(): void { + this.activeController.value?.showPreviousInlineCompletion(); + } +} + +// TODO: This should be local state to the editor. +// The global state should depend on the local state of the currently focused editor. +// Currently the global state is updated directly, which may lead to conflicts if multiple ghost texts are active. +class GhostTextContextKeys { + private lastInlineCompletionVisibleValue = false; + private readonly inlineCompletionVisible = GhostTextController.inlineSuggestionVisible.bindTo(this.contextKeyService); + + private lastInlineCompletionSuggestsIndentationValue = false; + private readonly inlineCompletionSuggestsIndentation = GhostTextController.inlineSuggestionHasIndentation.bindTo(this.contextKeyService); + + constructor(private readonly contextKeyService: IContextKeyService) { + } + + public setInlineCompletionVisible(value: boolean): void { + // Only modify the context key if we actually changed it. + // Thus, we don't overwrite values set by someone else. + if (value !== this.lastInlineCompletionVisibleValue) { + this.inlineCompletionVisible.set(value); + this.lastInlineCompletionVisibleValue = value; + } + } + + public setInlineCompletionSuggestsIndentation(value: boolean): void { + if (value !== this.lastInlineCompletionSuggestsIndentationValue) { + this.inlineCompletionSuggestsIndentation.set(value); + this.lastInlineCompletionSuggestsIndentationValue = value; + } + } +} + +/** + * The controller for a text editor with an initialized text model. +*/ +export class ActiveGhostTextController extends Disposable { + private readonly suggestWidgetAdapterModel = this._register(new SuggestWidgetAdapterModel(this.editor)); + private readonly inlineCompletionsModel = this._register(new InlineCompletionsModel(this.editor, this.commandService)); + + private get activeInlineCompletionsModel(): InlineCompletionsModel | undefined { + if (this.widget.model === this.inlineCompletionsModel) { + return this.inlineCompletionsModel; + } + return undefined; + } + + constructor( + private readonly editor: IActiveCodeEditor, + private readonly widget: GhostTextWidget, + private readonly contextKeys: GhostTextContextKeys, + @ICommandService private readonly commandService: ICommandService, + ) { + super(); + + this._register(this.suggestWidgetAdapterModel.onDidChange(() => { + this.updateModel(); + // When the suggest widget becomes inactive and an inline completion + // becomes visible, we need to update the context keys. + this.updateContextKeys(); + })); + this.updateModel(); + + this._register(toDisposable(() => { + if (widget.model === this.suggestWidgetAdapterModel || widget.model === this.inlineCompletionsModel) { + widget.setModel(undefined); + } + this.contextKeys.setInlineCompletionVisible(false); + this.contextKeys.setInlineCompletionSuggestsIndentation(false); + })); + + if (this.inlineCompletionsModel) { + this._register(this.inlineCompletionsModel.onDidChange(() => { + this.updateContextKeys(); + })); + } + } + + private updateContextKeys(): void { + this.contextKeys.setInlineCompletionVisible( + this.activeInlineCompletionsModel?.ghostText !== undefined + ); + + if (this.inlineCompletionsModel?.ghostText) { + const firstLine = this.inlineCompletionsModel.ghostText.lines[0] || ''; + const suggestionStartsWithWs = firstLine.startsWith(' ') || firstLine.startsWith('\t'); + const p = this.inlineCompletionsModel.ghostText.position; + const indentationEndColumn = this.editor.getModel().getLineIndentColumn(p.lineNumber); + const inIndentation = p.column <= indentationEndColumn; + + this.contextKeys.setInlineCompletionSuggestsIndentation( + this.widget.model === this.inlineCompletionsModel + && suggestionStartsWithWs && inIndentation + ); + } else { + this.contextKeys.setInlineCompletionSuggestsIndentation(false); + } + } + + public shouldShowHoverAt(hoverRange: Range): boolean { + const ghostText = this.activeInlineCompletionsModel?.ghostText; + if (ghostText) { + return hoverRange.containsPosition(ghostText.position); + } + return false; + } + + public triggerInlineCompletion(): void { + this.activeInlineCompletionsModel?.startSession(); + } + + public commitInlineCompletion(): void { + this.activeInlineCompletionsModel?.commitCurrentSuggestion(); + } + + public hideInlineCompletion(): void { + this.activeInlineCompletionsModel?.hide(); + } + + public showNextInlineCompletion(): void { + this.activeInlineCompletionsModel?.showNext(); + } + + public showPreviousInlineCompletion(): void { + this.activeInlineCompletionsModel?.showPrevious(); + } + + private updateModel() { + this.widget.setModel( + this.suggestWidgetAdapterModel.isActive + ? this.suggestWidgetAdapterModel + : this.inlineCompletionsModel + ); + this.inlineCompletionsModel?.setActive(this.widget.model === this.inlineCompletionsModel); + } +} + +const GhostTextCommand = EditorCommand.bindToContribution(GhostTextController.get); + +export const commitInlineSuggestionAction = new GhostTextCommand({ + id: 'editor.action.inlineSuggest.commit', + precondition: ContextKeyExpr.and( + GhostTextController.inlineSuggestionVisible, + GhostTextController.inlineSuggestionHasIndentation.toNegated(), + EditorContextKeys.tabMovesFocus.toNegated() + ), + kbOpts: { + weight: 100, + primary: KeyCode.Tab, + }, + handler(x) { + x.commit(); + x.editor.focus(); + } +}); +registerEditorCommand(commitInlineSuggestionAction); + +registerEditorCommand(new GhostTextCommand({ + id: 'editor.action.inlineSuggest.hide', + precondition: GhostTextController.inlineSuggestionVisible, + kbOpts: { + weight: 100, + primary: KeyCode.Escape, + }, + handler(x) { + x.hide(); + } +})); + +export class ShowNextInlineSuggestionAction extends EditorAction { + public static ID = 'editor.action.inlineSuggest.showNext'; + constructor() { + super({ + id: ShowNextInlineSuggestionAction.ID, + label: nls.localize('action.inlineSuggest.showNext', "Show Next Inline Suggestion"), + alias: 'Show Next Inline Suggestion', + precondition: EditorContextKeys.writable, + kbOpts: { + weight: 100, + primary: KeyMod.Alt | KeyCode.US_CLOSE_SQUARE_BRACKET, + }, + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + const controller = GhostTextController.get(editor); + if (controller) { + controller.showNextInlineCompletion(); + editor.focus(); + } + } +} + +export class ShowPreviousInlineSuggestionAction extends EditorAction { + public static ID = 'editor.action.inlineSuggest.showPrevious'; + constructor() { + super({ + id: ShowPreviousInlineSuggestionAction.ID, + label: nls.localize('action.inlineSuggest.showPrevious', "Show Previous Inline Suggestion"), + alias: 'Show Previous Inline Suggestion', + precondition: EditorContextKeys.writable, + kbOpts: { + weight: 100, + primary: KeyMod.Alt | KeyCode.US_OPEN_SQUARE_BRACKET, + }, + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + const controller = GhostTextController.get(editor); + if (controller) { + controller.showPreviousInlineCompletion(); + editor.focus(); + } + } +} + +export class TriggerInlineSuggestionAction extends EditorAction { + constructor() { + super({ + id: 'editor.action.inlineSuggest.trigger', + label: nls.localize('action.inlineSuggest.trigger', "Trigger Inline Suggestion"), + alias: 'Trigger Inline Suggestion', + precondition: EditorContextKeys.writable + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + const controller = GhostTextController.get(editor); + if (controller) { + controller.trigger(); + } + } +} + +registerEditorContribution(GhostTextController.ID, GhostTextController); +registerEditorAction(TriggerInlineSuggestionAction); +registerEditorAction(ShowNextInlineSuggestionAction); +registerEditorAction(ShowPreviousInlineSuggestionAction); diff --git a/src/vs/editor/contrib/inlineCompletions/ghostTextWidget.ts b/src/vs/editor/contrib/inlineCompletions/ghostTextWidget.ts new file mode 100644 index 0000000000..1cbbf1c752 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/ghostTextWidget.ts @@ -0,0 +1,427 @@ +/*--------------------------------------------------------------------------------------------- + * 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!./ghostText'; +import * as dom from 'vs/base/browser/dom'; +import { Disposable, DisposableStore, IDisposable, IReference, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Range } from 'vs/editor/common/core/range'; +import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import * as strings from 'vs/base/common/strings'; +import { RenderLineInput, renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer'; +import { EditorFontLigatures, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { createStringBuilder } from 'vs/editor/common/core/stringBuilder'; +import { Configuration } from 'vs/editor/browser/config/configuration'; +import { LineTokens } from 'vs/editor/common/core/lineTokens'; +import { Position } from 'vs/editor/common/core/position'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IModelDeltaDecoration } from 'vs/editor/common/model'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { ghostTextBorder, ghostTextForeground } from 'vs/editor/common/view/editorColorRegistry'; +import { RGBA, Color } from 'vs/base/common/color'; +import { CursorColumns } from 'vs/editor/common/controller/cursorCommon'; + +const ttPolicy = window.trustedTypes?.createPolicy('editorGhostText', { createHTML: value => value }); + +export interface GhostTextWidgetModel { + readonly onDidChange: Event; + readonly ghostText: GhostText | undefined; + + setExpanded(expanded: boolean): void; + readonly expanded: boolean; + + readonly minReservedLineCount: number; +} + +export interface GhostText { + readonly lines: string[]; + readonly position: Position; +} + +export abstract class BaseGhostTextWidgetModel extends Disposable implements GhostTextWidgetModel { + public abstract readonly ghostText: GhostText | undefined; + + private _expanded: boolean | undefined = undefined; + + protected readonly onDidChangeEmitter = new Emitter(); + public readonly onDidChange = this.onDidChangeEmitter.event; + + public abstract readonly minReservedLineCount: number; + + public get expanded() { + if (this._expanded === undefined) { + // TODO this should use a global hidden setting. + // See https://github.com/microsoft/vscode/issues/125037. + return true; + } + return this._expanded; + } + + constructor(protected readonly editor: IActiveCodeEditor) { + super(); + + this._register(editor.onDidChangeConfiguration((e) => { + if (e.hasChanged(EditorOption.suggest) && this._expanded === undefined) { + this.onDidChangeEmitter.fire(); + } + })); + } + + public setExpanded(expanded: boolean): void { + this._expanded = true; + this.onDidChangeEmitter.fire(); + } +} + +export class GhostTextWidget extends Disposable { + private static decorationTypeCount = 0; + + private codeEditorDecorationTypeKey: string | null = null; + private readonly modelRef = this._register(new MutableDisposable>()); + private decorationIds: string[] = []; + private viewZoneId: string | null = null; + private viewMoreContentWidget: ViewMoreLinesContentWidget | null = null; + + constructor( + private readonly editor: ICodeEditor, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, + @IThemeService private readonly _themeService: IThemeService, + ) { + super(); + + this._register(this.editor.onDidChangeConfiguration((e) => { + if ( + e.hasChanged(EditorOption.disableMonospaceOptimizations) + || e.hasChanged(EditorOption.stopRenderingLineAfter) + || e.hasChanged(EditorOption.renderWhitespace) + || e.hasChanged(EditorOption.renderControlCharacters) + || e.hasChanged(EditorOption.fontLigatures) + || e.hasChanged(EditorOption.fontInfo) + || e.hasChanged(EditorOption.lineHeight) + ) { + this.render(); + } + })); + this._register(toDisposable(() => { + this.setModel(undefined); + })); + } + + public get model(): GhostTextWidgetModel | undefined { + return this.modelRef.value?.object; + } + + public shouldShowHoverAtViewZone(viewZoneId: string): boolean { + return (this.viewZoneId === viewZoneId); + } + + public setModel(model: GhostTextWidgetModel | undefined): void { + if (model === this.model) { return; } + this.modelRef.value = model + ? createDisposableRef(model, model.onDidChange(() => this.render())) + : undefined; + this.render(); + } + + private getRenderData() { + if (!this.editor.hasModel() || !this.model?.ghostText) { + return undefined; + } + + const { minReservedLineCount, expanded } = this.model; + let { position, lines } = this.model.ghostText; + + const textModel = this.editor.getModel(); + const maxColumn = textModel.getLineMaxColumn(position.lineNumber); + const { tabSize } = textModel.getOptions(); + + if (lines.length > 1 && position.column !== maxColumn) { + console.warn('Can only show multiline ghost text at the end of a line'); + lines = []; + position = new Position(position.lineNumber, maxColumn); + } + + return { tabSize, position, lines, minReservedLineCount, expanded }; + } + + private render(): void { + const renderData = this.getRenderData(); + + if (this.codeEditorDecorationTypeKey) { + this._codeEditorService.removeDecorationType(this.codeEditorDecorationTypeKey); + this.codeEditorDecorationTypeKey = null; + } + + if (renderData && renderData.lines.length > 0) { + const foreground = this._themeService.getColorTheme().getColor(ghostTextForeground); + let opacity: string | undefined = undefined; + let color: string | undefined = undefined; + if (foreground) { + function opaque(color: Color): Color { + const { r, b, g } = color.rgba; + return new Color(new RGBA(r, g, b, 255)); + } + + opacity = String(foreground.rgba.a); + color = Color.Format.CSS.format(opaque(foreground))!; + } + + const borderColor = this._themeService.getColorTheme().getColor(ghostTextBorder); + let border: string | undefined = undefined; + if (borderColor) { + border = `2px dashed ${borderColor}`; + } + + // We add 0 to bring it before any other decoration. + this.codeEditorDecorationTypeKey = `0-ghost-text-${++GhostTextWidget.decorationTypeCount}`; + + const line = this.editor.getModel()?.getLineContent(renderData.position.lineNumber) || ''; + const linePrefix = line.substr(0, renderData.position.column - 1); + + // To avoid visual confusion, we don't want to render visible whitespace + const contentText = renderSingleLineText(renderData.lines[0] || '', linePrefix, renderData.tabSize, false); + + this._codeEditorService.registerDecorationType('ghost-text', this.codeEditorDecorationTypeKey, { + after: { + // TODO: escape? + contentText, + opacity, + color, + border, + }, + }); + } + + const newDecorations = new Array(); + if (renderData && this.codeEditorDecorationTypeKey) { + newDecorations.push({ + range: Range.fromPositions(renderData.position, renderData.position), + options: { + ...this._codeEditorService.resolveDecorationOptions(this.codeEditorDecorationTypeKey, true), + } + }); + } + this.decorationIds = this.editor.deltaDecorations(this.decorationIds, newDecorations); + + if (this.viewMoreContentWidget) { + this.viewMoreContentWidget.dispose(); + this.viewMoreContentWidget = null; + } + + this.editor.changeViewZones((changeAccessor) => { + if (this.viewZoneId) { + changeAccessor.removeZone(this.viewZoneId); + this.viewZoneId = null; + } + + if (renderData) { + const remainingLines = renderData.lines.slice(1); + const heightInLines = Math.max(remainingLines.length, renderData.minReservedLineCount); + if (heightInLines > 0) { + if (renderData.expanded) { + const domNode = document.createElement('div'); + this.renderLines(domNode, renderData.tabSize, remainingLines); + + this.viewZoneId = changeAccessor.addZone({ + afterLineNumber: renderData.position.lineNumber, + afterColumn: renderData.position.column, + heightInLines: heightInLines, + domNode, + }); + } else if (remainingLines.length > 0) { + this.viewMoreContentWidget = this.renderViewMoreLines(renderData.position, renderData.lines[0], remainingLines.length); + } + } + } + }); + } + + private renderViewMoreLines(position: Position, firstLineText: string, remainingLinesLength: number): ViewMoreLinesContentWidget { + const fontInfo = this.editor.getOption(EditorOption.fontInfo); + const domNode = document.createElement('div'); + domNode.className = 'suggest-preview-additional-widget'; + Configuration.applyFontInfoSlow(domNode, fontInfo); + + const spacer = document.createElement('span'); + spacer.className = 'content-spacer'; + spacer.append(firstLineText); + domNode.append(spacer); + + const newline = document.createElement('span'); + newline.className = 'content-newline suggest-preview-text'; + newline.append('⏎ '); + domNode.append(newline); + + const disposableStore = new DisposableStore(); + + const button = document.createElement('div'); + button.className = 'button suggest-preview-text'; + button.append(`+${remainingLinesLength} lines…`); + + disposableStore.add(dom.addStandardDisposableListener(button, 'mousedown', (e) => { + this.model?.setExpanded(true); + e.preventDefault(); + this.editor.focus(); + })); + + domNode.append(button); + return new ViewMoreLinesContentWidget(this.editor, position, domNode, disposableStore); + } + + private renderLines(domNode: HTMLElement, tabSize: number, lines: string[]): void { + const opts = this.editor.getOptions(); + const disableMonospaceOptimizations = opts.get(EditorOption.disableMonospaceOptimizations); + const stopRenderingLineAfter = opts.get(EditorOption.stopRenderingLineAfter); + // To avoid visual confusion, we don't want to render visible whitespace + const renderWhitespace = 'none'; + const renderControlCharacters = opts.get(EditorOption.renderControlCharacters); + const fontLigatures = opts.get(EditorOption.fontLigatures); + const fontInfo = opts.get(EditorOption.fontInfo); + const lineHeight = opts.get(EditorOption.lineHeight); + + const sb = createStringBuilder(10000); + sb.appendASCIIString('
'); + + for (let i = 0, len = lines.length; i < len; i++) { + const line = lines[i]; + sb.appendASCIIString('
'); + + const isBasicASCII = strings.isBasicASCII(line); + const containsRTL = strings.containsRTL(line); + const lineTokens = LineTokens.createEmpty(line); + + renderViewLine(new RenderLineInput( + (fontInfo.isMonospace && !disableMonospaceOptimizations), + fontInfo.canUseHalfwidthRightwardsArrow, + line, + false, + isBasicASCII, + containsRTL, + 0, + lineTokens, + [], + tabSize, + 0, + fontInfo.spaceWidth, + fontInfo.middotWidth, + fontInfo.wsmiddotWidth, + stopRenderingLineAfter, + renderWhitespace, + renderControlCharacters, + fontLigatures !== EditorFontLigatures.OFF, + null + ), sb); + + sb.appendASCIIString('
'); + } + sb.appendASCIIString('
'); + + Configuration.applyFontInfoSlow(domNode, fontInfo); + const html = sb.build(); + const trustedhtml = ttPolicy ? ttPolicy.createHTML(html) : html; + domNode.innerHTML = trustedhtml as string; + } +} + +function renderSingleLineText(text: string, lineStart: string, tabSize: number, renderWhitespace: boolean): string { + const newLine = lineStart + text; + const visibleColumnsByColumns = CursorColumns.visibleColumnsByColumns(newLine, tabSize); + + + let contentText = ''; + let curCol = lineStart.length + 1; + for (const c of text) { + if (c === '\t') { + const width = visibleColumnsByColumns[curCol + 1] - visibleColumnsByColumns[curCol]; + if (renderWhitespace) { + contentText += '→'; + for (let i = 1; i < width; i++) { + contentText += '\xa0'; + } + } else { + for (let i = 0; i < width; i++) { + contentText += '\xa0'; + } + } + } else if (c === ' ') { + if (renderWhitespace) { + contentText += '·'; + } else { + contentText += '\xa0'; + } + } else { + contentText += c; + } + curCol += 1; + } + + return contentText; +} + +class ViewMoreLinesContentWidget extends Disposable implements IContentWidget { + readonly allowEditorOverflow = false; + readonly suppressMouseDown = false; + + constructor( + private editor: ICodeEditor, + private position: Position, + private domNode: HTMLElement, + disposableStore: DisposableStore + ) { + super(); + this._register(disposableStore); + this._register(toDisposable(() => { + this.editor.removeContentWidget(this); + })); + this.editor.addContentWidget(this); + } + + getId(): string { + return 'editor.widget.viewMoreLinesWidget'; + } + + getDomNode(): HTMLElement { + return this.domNode; + } + + getPosition(): IContentWidgetPosition | null { + return { + position: this.position, + preference: [ContentWidgetPositionPreference.EXACT] + }; + } +} + +registerThemingParticipant((theme, collector) => { + const foreground = theme.getColor(ghostTextForeground); + + if (foreground) { + function opaque(color: Color): Color { + const { r, b, g } = color.rgba; + return new Color(new RGBA(r, g, b, 255)); + } + + const opacity = String(foreground.rgba.a); + const color = Color.Format.CSS.format(opaque(foreground))!; + + // We need to override the only used token type .mtk1 + collector.addRule(`.monaco-editor .suggest-preview-text .mtk1 { opacity: ${opacity}; color: ${color}; }`); + } + + const border = theme.getColor(ghostTextBorder); + if (border) { + collector.addRule(`.monaco-editor .suggest-preview-text .mtk1 { border: 2px dashed ${border}; }`); + } +}); + +function createDisposableRef(object: T, disposable: IDisposable): IReference { + return { + object, + dispose: () => disposable.dispose(), + }; +} diff --git a/src/vs/editor/contrib/inlineCompletions/inlineCompletionsHoverParticipant.ts b/src/vs/editor/contrib/inlineCompletions/inlineCompletionsHoverParticipant.ts new file mode 100644 index 0000000000..b89740842e --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/inlineCompletionsHoverParticipant.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 * as nls from 'vs/nls'; +import { HoverAnchor, HoverAnchorType, HoverForeignElementAnchor, IEditorHover, IEditorHoverParticipant, IEditorHoverStatusBar, IHoverPart } from 'vs/editor/contrib/hover/hoverTypes'; +import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import { IModelDecoration } from 'vs/editor/common/model'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { commitInlineSuggestionAction, GhostTextController, ShowNextInlineSuggestionAction, ShowPreviousInlineSuggestionAction } from 'vs/editor/contrib/inlineCompletions/ghostTextController'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ITextContentData, IViewZoneData } from 'vs/editor/browser/controller/mouseTarget'; + +export class InlineCompletionsHover implements IHoverPart { + constructor( + public readonly owner: IEditorHoverParticipant, + public readonly range: Range + ) { } + + public isValidForHoverAnchor(anchor: HoverAnchor): boolean { + return ( + anchor.type === HoverAnchorType.Range + && this.range.startColumn <= anchor.range.startColumn + && this.range.endColumn >= anchor.range.endColumn + ); + } +} + +export class InlineCompletionsHoverParticipant implements IEditorHoverParticipant { + constructor( + private readonly _editor: ICodeEditor, + hover: IEditorHover, + @ICommandService private readonly _commandService: ICommandService, + @IMenuService private readonly _menuService: IMenuService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + ) { } + + suggestHoverAnchor(mouseEvent: IEditorMouseEvent): HoverAnchor | null { + const controller = GhostTextController.get(this._editor); + if (!controller) { + return null; + } + if (mouseEvent.target.type === MouseTargetType.CONTENT_VIEW_ZONE) { + // handle the case where the mouse is over the view zone + const viewZoneData = mouseEvent.target.detail; + if (controller.shouldShowHoverAtViewZone(viewZoneData.viewZoneId)) { + return new HoverForeignElementAnchor(1000, this, Range.fromPositions(viewZoneData.positionBefore || viewZoneData.position, viewZoneData.positionBefore || viewZoneData.position)); + } + } + if (mouseEvent.target.type === MouseTargetType.CONTENT_EMPTY && mouseEvent.target.range) { + // handle the case where the mouse is over the empty portion of a line following ghost text + if (controller.shouldShowHoverAt(mouseEvent.target.range)) { + return new HoverForeignElementAnchor(1000, this, mouseEvent.target.range); + } + } + if (mouseEvent.target.type === MouseTargetType.CONTENT_TEXT && mouseEvent.target.range && mouseEvent.target.detail) { + // handle the case where the mouse is directly over ghost text + const mightBeForeignElement = (mouseEvent.target.detail).mightBeForeignElement; + if (mightBeForeignElement && controller.shouldShowHoverAt(mouseEvent.target.range)) { + return new HoverForeignElementAnchor(1000, this, mouseEvent.target.range); + } + } + return null; + } + + computeSync(anchor: HoverAnchor, lineDecorations: IModelDecoration[]): InlineCompletionsHover[] { + const controller = GhostTextController.get(this._editor); + if (controller && controller.shouldShowHoverAt(anchor.range)) { + return [new InlineCompletionsHover(this, anchor.range)]; + } + return []; + } + + renderHoverParts(hoverParts: InlineCompletionsHover[], fragment: DocumentFragment, statusBar: IEditorHoverStatusBar): IDisposable { + const menu = this._menuService.createMenu( + MenuId.InlineCompletionsActions, + this._contextKeyService + ); + + statusBar.addAction({ + label: nls.localize('showNextInlineSuggestion', "Next"), + commandId: ShowNextInlineSuggestionAction.ID, + run: () => this._commandService.executeCommand(ShowNextInlineSuggestionAction.ID) + }); + statusBar.addAction({ + label: nls.localize('showPreviousInlineSuggestion', "Previous"), + commandId: ShowPreviousInlineSuggestionAction.ID, + run: () => this._commandService.executeCommand(ShowPreviousInlineSuggestionAction.ID) + }); + statusBar.addAction({ + label: nls.localize('acceptInlineSuggestion', "Accept"), + commandId: commitInlineSuggestionAction.id, + run: () => this._commandService.executeCommand(commitInlineSuggestionAction.id) + }); + + for (const [_, group] of menu.getActions()) { + for (const action of group) { + if (action instanceof MenuItemAction) { + statusBar.addAction({ + label: action.label, + commandId: action.item.id, + run: () => this._commandService.executeCommand(action.item.id) + }); + } + } + } + + return Disposable.None; + } +} diff --git a/src/vs/editor/contrib/inlineCompletions/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/inlineCompletionsModel.ts new file mode 100644 index 0000000000..9f4832ea39 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/inlineCompletionsModel.ts @@ -0,0 +1,592 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancelablePromise, createCancelablePromise, RunOnceScheduler } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import * as strings from 'vs/base/common/strings'; +import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { ITextModel } from 'vs/editor/common/model'; +import { InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider, InlineCompletionsProviderRegistry, InlineCompletionTriggerKind } from 'vs/editor/common/modes'; +import { BaseGhostTextWidgetModel, GhostText, GhostTextWidgetModel } from 'vs/editor/contrib/inlineCompletions/ghostTextWidget'; +import { EditOperation } from 'vs/editor/common/core/editOperation'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { MutableDisposable } from 'vs/editor/contrib/inlineCompletions/utils'; +import { RedoCommand, UndoCommand } from 'vs/editor/browser/editorExtensions'; +import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; + +export class InlineCompletionsModel extends Disposable implements GhostTextWidgetModel { + protected readonly onDidChangeEmitter = new Emitter(); + public readonly onDidChange = this.onDidChangeEmitter.event; + + private readonly completionSession = this._register(new MutableDisposable()); + + private active: boolean = false; + + constructor( + private readonly editor: IActiveCodeEditor, + private readonly commandService: ICommandService + ) { + super(); + + this._register(commandService.onDidExecuteCommand(e => { + // These commands don't trigger onDidType. + const commands = new Set([ + UndoCommand.id, + RedoCommand.id, + CoreEditingCommands.Tab.id, + CoreEditingCommands.DeleteLeft.id, + CoreEditingCommands.DeleteRight.id + ]); + if (commands.has(e.commandId) && editor.hasTextFocus()) { + this.handleUserInput(); + } + })); + + this._register(this.editor.onDidType((e) => { + this.handleUserInput(); + })); + + this._register(this.editor.onDidChangeCursorPosition((e) => { + if (this.session && !this.session.isValid) { + this.hide(); + } + })); + } + + private handleUserInput() { + if (this.session && !this.session.isValid) { + this.hide(); + } + setTimeout(() => { + // Wait for the cursor update that happens in the same iteration loop iteration + this.startSessionIfTriggered(); + }, 0); + } + + private get session(): InlineCompletionsSession | undefined { + return this.completionSession.value; + } + + public get ghostText(): GhostText | undefined { + return this.session?.ghostText; + } + + public get minReservedLineCount(): number { + return this.session ? this.session.minReservedLineCount : 0; + } + + public get expanded(): boolean { + return this.session ? this.session.expanded : false; + } + + public setExpanded(expanded: boolean): void { + this.session?.setExpanded(expanded); + } + + public setActive(active: boolean) { + this.active = active; + if (active) { + this.session?.scheduleAutomaticUpdate(); + } + } + + private startSessionIfTriggered(): void { + const suggestOptions = this.editor.getOption(EditorOption.inlineSuggest); + if (!suggestOptions.enabled) { + return; + } + + if (this.session && this.session.isValid) { + return; + } + + this.startSession(); + } + + public startSession(): void { + if (this.completionSession.value) { + return; + } + this.completionSession.value = new InlineCompletionsSession(this.editor, this.editor.getPosition(), () => this.active, this.commandService); + this.completionSession.value.takeOwnership( + this.completionSession.value.onDidChange(() => { + this.onDidChangeEmitter.fire(); + }) + ); + } + + public hide(): void { + this.completionSession.clear(); + this.onDidChangeEmitter.fire(); + } + + public commitCurrentSuggestion(): void { + // Don't dispose the session, so that after committing, more suggestions are shown. + this.session?.commitCurrentCompletion(); + } + + public showNext(): void { + this.session?.showNextInlineCompletion(); + } + + public showPrevious(): void { + this.session?.showPreviousInlineCompletion(); + } +} + +class InlineCompletionsSession extends BaseGhostTextWidgetModel { + public readonly minReservedLineCount = 0; + + private readonly updateOperation = this._register(new MutableDisposable()); + private readonly cache = this._register(new MutableDisposable()); + + private updateSoon = this._register(new RunOnceScheduler(() => this.update(InlineCompletionTriggerKind.Automatic), 50)); + private readonly textModel = this.editor.getModel(); + + constructor( + editor: IActiveCodeEditor, + private readonly triggerPosition: Position, + private readonly shouldUpdate: () => boolean, + private readonly commandService: ICommandService, + ) { + super(editor); + + let lastCompletionItem: InlineCompletion | undefined = undefined; + this._register(this.onDidChange(() => { + const currentCompletion = this.currentCompletion; + if (currentCompletion && currentCompletion.sourceInlineCompletion !== lastCompletionItem) { + lastCompletionItem = currentCompletion.sourceInlineCompletion; + + const provider = currentCompletion.sourceProvider; + if (provider.handleItemDidShow) { + provider.handleItemDidShow(currentCompletion.sourceInlineCompletions, lastCompletionItem); + } + } + })); + + this._register(this.editor.onDidChangeModelContent((e) => { + if (this.cache.value) { + let hasChanged = false; + for (const c of this.cache.value.completions) { + const newRange = this.textModel.getDecorationRange(c.decorationId); + if (!newRange) { + onUnexpectedError(new Error('Decoration has no range')); + continue; + } + if (!c.synchronizedRange.equalsRange(newRange)) { + hasChanged = true; + c.synchronizedRange = newRange; + } + } + if (hasChanged) { + this.onDidChangeEmitter.fire(); + } + } + + this.scheduleAutomaticUpdate(); + })); + + this.scheduleAutomaticUpdate(); + } + + //#region Selection + + // We use a semantic id to track the selection even if the cache changes. + private currentlySelectedCompletionId: string | undefined = undefined; + + private fixAndGetIndexOfCurrentSelection(): number { + if (!this.currentlySelectedCompletionId || !this.cache.value) { + return 0; + } + if (this.cache.value.completions.length === 0) { + // don't reset the selection in this case + return 0; + } + + const idx = this.cache.value.completions.findIndex(v => v.semanticId === this.currentlySelectedCompletionId); + if (idx === -1) { + // Reset the selection so that the selection does not jump back when it appears again + this.currentlySelectedCompletionId = undefined; + return 0; + } + return idx; + } + + private get currentCachedCompletion(): CachedInlineCompletion | undefined { + if (!this.cache.value) { + return undefined; + } + return this.cache.value.completions[this.fixAndGetIndexOfCurrentSelection()]; + } + + public async showNextInlineCompletion(): Promise { + await this.ensureUpdateWithExplicitContext(); + + const completions = this.cache.value?.completions || []; + if (completions.length > 0) { + const newIdx = (this.fixAndGetIndexOfCurrentSelection() + 1) % completions.length; + this.currentlySelectedCompletionId = completions[newIdx].semanticId; + } else { + this.currentlySelectedCompletionId = undefined; + } + this.onDidChangeEmitter.fire(); + } + + public async showPreviousInlineCompletion(): Promise { + await this.ensureUpdateWithExplicitContext(); + + const completions = this.cache.value?.completions || []; + if (completions.length > 0) { + const newIdx = (this.fixAndGetIndexOfCurrentSelection() + completions.length - 1) % completions.length; + this.currentlySelectedCompletionId = completions[newIdx].semanticId; + } else { + this.currentlySelectedCompletionId = undefined; + } + this.onDidChangeEmitter.fire(); + } + + private async ensureUpdateWithExplicitContext(): Promise { + if (this.updateOperation.value) { + // Restart or wait for current update operation + if (this.updateOperation.value.triggerKind === InlineCompletionTriggerKind.Explicit) { + await this.updateOperation.value.promise; + } else { + await this.update(InlineCompletionTriggerKind.Explicit); + } + } else if (this.cache.value?.triggerKind !== InlineCompletionTriggerKind.Explicit) { + // Refresh cache + await this.update(InlineCompletionTriggerKind.Explicit); + } + } + + //#endregion + + public get ghostText(): GhostText | undefined { + const currentCompletion = this.currentCompletion; + return currentCompletion ? inlineCompletionToGhostText(currentCompletion, this.editor.getModel()) : undefined; + } + + get currentCompletion(): LiveInlineCompletion | undefined { + const completion = this.currentCachedCompletion; + if (!completion) { + return undefined; + } + return { + text: completion.inlineCompletion.text, + range: completion.synchronizedRange, + command: completion.inlineCompletion.command, + sourceProvider: completion.inlineCompletion.sourceProvider, + sourceInlineCompletions: completion.inlineCompletion.sourceInlineCompletions, + sourceInlineCompletion: completion.inlineCompletion.sourceInlineCompletion, + }; + } + + get isValid(): boolean { + return this.editor.getPosition().lineNumber === this.triggerPosition.lineNumber; + } + + public scheduleAutomaticUpdate(): void { + // Since updateSoon debounces, starvation can happen. + // To prevent stale cache, we clear the current update operation. + this.updateOperation.clear(); + this.updateSoon.schedule(); + } + + private async update(triggerKind: InlineCompletionTriggerKind): Promise { + if (!this.shouldUpdate()) { + return; + } + + const position = this.editor.getPosition(); + + const promise = createCancelablePromise(async token => { + let result; + try { + result = await provideInlineCompletions(position, + this.editor.getModel(), + { triggerKind }, + token + ); + } catch (e) { + onUnexpectedError(e); + return; + } + + if (token.isCancellationRequested) { + return; + } + + this.cache.value = new SynchronizedInlineCompletionsCache( + this.editor, + result, + () => this.onDidChangeEmitter.fire(), + triggerKind + ); + this.onDidChangeEmitter.fire(); + }); + const operation = new UpdateOperation(promise, triggerKind); + this.updateOperation.value = operation; + await promise; + if (this.updateOperation.value === operation) { + this.updateOperation.clear(); + } + } + + public takeOwnership(disposable: IDisposable): void { + this._register(disposable); + } + + public commitCurrentCompletion(): void { + const completion = this.currentCompletion; + if (completion) { + this.commit(completion); + } + } + + public commit(completion: LiveInlineCompletion): void { + // Mark the cache as stale, but don't dispose it yet, + // otherwise command args might get disposed. + const cache = this.cache.replace(undefined); + + this.editor.executeEdits( + 'inlineSuggestion.accept', + [ + EditOperation.replaceMove(completion.range, completion.text) + ] + ); + if (completion.command) { + this.commandService + .executeCommand(completion.command.id, ...(completion.command.arguments || [])) + .finally(() => { + cache?.dispose(); + }) + .then(undefined, onUnexpectedExternalError); + } else { + cache?.dispose(); + } + + this.onDidChangeEmitter.fire(); + } +} + +class UpdateOperation implements IDisposable { + constructor(public readonly promise: CancelablePromise, public readonly triggerKind: InlineCompletionTriggerKind) { + } + + dispose() { + this.promise.cancel(); + } +} + +/** + * The cache keeps itself in sync with the editor. + * It also owns the completions result and disposes it when the cache is diposed. +*/ +class SynchronizedInlineCompletionsCache extends Disposable { + public readonly completions: readonly CachedInlineCompletion[]; + + constructor( + editor: IActiveCodeEditor, + completionsSource: LiveInlineCompletions, + onChange: () => void, + public readonly triggerKind: InlineCompletionTriggerKind, + ) { + super(); + + const decorationIds = editor.deltaDecorations( + [], + completionsSource.items.map(i => ({ + range: i.range, + options: { + description: 'inline-completion-tracking-range' + }, + })) + ); + this._register(toDisposable(() => { + editor.deltaDecorations(decorationIds, []); + })); + + this.completions = completionsSource.items.map((c, idx) => new CachedInlineCompletion(c, decorationIds[idx])); + + this._register(editor.onDidChangeModelContent(() => { + let hasChanged = false; + const model = editor.getModel(); + for (const c of this.completions) { + const newRange = model.getDecorationRange(c.decorationId); + if (!newRange) { + onUnexpectedError(new Error('Decoration has no range')); + continue; + } + if (!c.synchronizedRange.equalsRange(newRange)) { + hasChanged = true; + c.synchronizedRange = newRange; + } + } + if (hasChanged) { + onChange(); + } + })); + + this._register(completionsSource); + } +} + +class CachedInlineCompletion { + public readonly semanticId: string = JSON.stringify({ + text: this.inlineCompletion.text, + startLine: this.inlineCompletion.range.startLineNumber, + startColumn: this.inlineCompletion.range.startColumn, + command: this.inlineCompletion.command + }); + /** + * The range, synchronized with text model changes. + */ + public synchronizedRange: Range; + + constructor( + public readonly inlineCompletion: LiveInlineCompletion, + public readonly decorationId: string, + ) { + this.synchronizedRange = inlineCompletion.range; + } +} + +export interface NormalizedInlineCompletion extends InlineCompletion { + range: Range; +} + +function leftTrim(str: string): string { + return str.replace(/^\s+/, ''); +} + +export function inlineCompletionToGhostText(inlineCompletion: NormalizedInlineCompletion, textModel: ITextModel): GhostText | undefined { + // This is a single line string + const valueToBeReplaced = textModel.getValueInRange(inlineCompletion.range); + + let remainingInsertText: string; + + // Consider these cases + // valueToBeReplaced -> inlineCompletion.text + // "\t\tfoo" -> "\t\tfoobar" (+"bar") + // "\t" -> "\t\tfoobar" (+"\tfoobar") + // "\t\tfoo" -> "\t\t\tfoobar" (+"\t", +"bar") + // "\t\tfoo" -> "\tfoobar" (-"\t", +"\bar") + + const firstNonWsCol = textModel.getLineFirstNonWhitespaceColumn(inlineCompletion.range.startLineNumber); + + if (inlineCompletion.text.startsWith(valueToBeReplaced)) { + remainingInsertText = inlineCompletion.text.substr(valueToBeReplaced.length); + } else if (firstNonWsCol === 0 || inlineCompletion.range.startColumn < firstNonWsCol) { + // Only allow ignoring leading whitespace in indentation. + const valueToBeReplacedTrimmed = leftTrim(valueToBeReplaced); + const insertTextTrimmed = leftTrim(inlineCompletion.text); + if (!insertTextTrimmed.startsWith(valueToBeReplacedTrimmed)) { + return undefined; + } + remainingInsertText = insertTextTrimmed.substr(valueToBeReplacedTrimmed.length); + } else { + return undefined; + } + + const position = inlineCompletion.range.getEndPosition(); + + const lines = strings.splitLines(remainingInsertText); + + if (lines.length > 1 && textModel.getLineMaxColumn(position.lineNumber) !== position.column) { + // Such ghost text is not supported. + return undefined; + } + + return { + lines, + position + }; +} + +export interface LiveInlineCompletion extends NormalizedInlineCompletion { + sourceProvider: InlineCompletionsProvider; + sourceInlineCompletion: InlineCompletion; + sourceInlineCompletions: InlineCompletions; +} + +/** + * Contains no duplicated items. +*/ +export interface LiveInlineCompletions extends InlineCompletions { + dispose(): void; +} + +function getDefaultRange(position: Position, model: ITextModel): Range { + const word = model.getWordAtPosition(position); + const maxColumn = model.getLineMaxColumn(position.lineNumber); + // By default, always replace up until the end of the current line. + // This default might be subject to change! + return word + ? new Range(position.lineNumber, word.startColumn, position.lineNumber, maxColumn) + : Range.fromPositions(position, position.with(undefined, maxColumn)); +} + +async function provideInlineCompletions( + position: Position, + model: ITextModel, + context: InlineCompletionContext, + token: CancellationToken = CancellationToken.None +): Promise { + const defaultReplaceRange = getDefaultRange(position, model); + + const providers = InlineCompletionsProviderRegistry.all(model); + const results = await Promise.all( + providers.map( + async provider => { + const completions = await provider.provideInlineCompletions(model, position, context, token); + return ({ + completions, + provider, + dispose: () => { + if (completions) { + provider.freeInlineCompletions(completions); + } + } + }); + } + ) + ); + + const itemsByHash = new Map(); + for (const result of results) { + const completions = result.completions; + if (completions) { + for (const item of completions.items.map(item => ({ + text: item.text, + range: item.range ? Range.lift(item.range) : defaultReplaceRange, + command: item.command, + sourceProvider: result.provider, + sourceInlineCompletions: completions, + sourceInlineCompletion: item + }))) { + if (item.range.startLineNumber !== item.range.endLineNumber) { + // Ignore invalid ranges. + continue; + } + itemsByHash.set(JSON.stringify({ text: item.text, range: item.range }), item); + } + } + } + + return { + items: [...itemsByHash.values()], + dispose: () => { + for (const result of results) { + result.dispose(); + } + }, + }; +} diff --git a/src/vs/editor/contrib/inlineCompletions/suggestWidgetAdapterModel.ts b/src/vs/editor/contrib/inlineCompletions/suggestWidgetAdapterModel.ts new file mode 100644 index 0000000000..83021a6b3d --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/suggestWidgetAdapterModel.ts @@ -0,0 +1,187 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Event } from 'vs/base/common/event'; +import { toDisposable } from 'vs/base/common/lifecycle'; +import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { CompletionItemInsertTextRule } from 'vs/editor/common/modes'; +import { BaseGhostTextWidgetModel, GhostText } from 'vs/editor/contrib/inlineCompletions/ghostTextWidget'; +import { inlineCompletionToGhostText, NormalizedInlineCompletion } from 'vs/editor/contrib/inlineCompletions/inlineCompletionsModel'; +import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser'; +import { SnippetSession } from 'vs/editor/contrib/snippet/snippetSession'; +import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; +import { ISelectedSuggestion } from 'vs/editor/contrib/suggest/suggestWidget'; + +export class SuggestWidgetAdapterModel extends BaseGhostTextWidgetModel { + private isSuggestWidgetVisible: boolean = false; + private currentGhostText: GhostText | undefined = undefined; + private _isActive: boolean = false; + + public override minReservedLineCount: number = 0; + + public get isActive() { return this._isActive; } + + // This delay fixes an suggest widget issue when typing "." immediately restarts the suggestion session. + private setInactiveDelayed = this._register(new RunOnceScheduler(() => { + if (!this.isSuggestWidgetVisible) { + if (this.isActive) { + this._isActive = false; + this.onDidChangeEmitter.fire(); + } + } + }, 100)); + + constructor( + editor: IActiveCodeEditor + ) { + super(editor); + + const suggestController = SuggestController.get(this.editor); + if (suggestController) { + let isBoundToSuggestWidget = false; + const bindToSuggestWidget = () => { + if (isBoundToSuggestWidget) { + return; + } + isBoundToSuggestWidget = true; + + this._register(suggestController.widget.value.onDidShow(() => { + this.isSuggestWidgetVisible = true; + this._isActive = true; + this.updateFromSuggestion(); + })); + this._register(suggestController.widget.value.onDidHide(() => { + this.isSuggestWidgetVisible = false; + this.setInactiveDelayed.schedule(); + this.minReservedLineCount = 0; + this.updateFromSuggestion(); + })); + this._register(suggestController.widget.value.onDidFocus(() => { + this.isSuggestWidgetVisible = true; + this._isActive = true; + this.updateFromSuggestion(); + })); + }; + + this._register(Event.once(suggestController.model.onDidTrigger)(e => { + bindToSuggestWidget(); + })); + } + this.updateFromSuggestion(); + + this._register(toDisposable(() => { + const suggestController = SuggestController.get(this.editor); + if (suggestController) { + suggestController.stopForceRenderingAbove(); + } + })); + } + + public override setExpanded(expanded: boolean): void { + super.setExpanded(expanded); + this.updateFromSuggestion(); + } + + private isSuggestionPreviewEnabled(): boolean { + const suggestOptions = this.editor.getOption(EditorOption.suggest); + return suggestOptions.preview; + } + + private updateFromSuggestion(): void { + const suggestController = SuggestController.get(this.editor); + if (!suggestController) { + this.setCurrentInlineCompletion(undefined); + return; + } + if (!this.isSuggestWidgetVisible) { + this.setCurrentInlineCompletion(undefined); + return; + } + const focusedItem = suggestController.widget.value.getFocusedItem(); + if (!focusedItem) { + this.setCurrentInlineCompletion(undefined); + return; + } + + // TODO: item.isResolved + this.setCurrentInlineCompletion( + getInlineCompletion( + suggestController, + this.editor.getPosition(), + focusedItem + ) + ); + } + + private setCurrentInlineCompletion(completion: NormalizedInlineCompletion | undefined): void { + this.currentGhostText = completion + ? ( + inlineCompletionToGhostText(completion, this.editor.getModel()) || + // Show an invisible ghost text to reserve space + { + lines: [], + position: completion.range.getEndPosition(), + } + ) : undefined; + + if (this.currentGhostText && this.expanded) { + this.minReservedLineCount = Math.max(this.minReservedLineCount, this.currentGhostText.lines.length - 1); + } + + const suggestController = SuggestController.get(this.editor); + if (suggestController) { + if (this.minReservedLineCount >= 1 && this.isSuggestionPreviewEnabled()) { + suggestController.forceRenderingAbove(); + } else { + suggestController.stopForceRenderingAbove(); + } + } + + this.onDidChangeEmitter.fire(); + } + + public override get ghostText(): GhostText | undefined { + return this.isSuggestionPreviewEnabled() + ? this.currentGhostText + : undefined; + } +} + +function getInlineCompletion(suggestController: SuggestController, position: Position, suggestion: ISelectedSuggestion): NormalizedInlineCompletion { + const item = suggestion.item; + + if (Array.isArray(item.completion.additionalTextEdits)) { + // cannot represent additional text edits + return { + text: '', + range: Range.fromPositions(position, position), + }; + } + + let { insertText } = item.completion; + if (item.completion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet) { + const snippet = new SnippetParser().parse(insertText); + const model = suggestController.editor.getModel()!; + SnippetSession.adjustWhitespace( + model, position, snippet, + true, + true + ); + insertText = snippet.toString(); + } + + const info = suggestController.getOverwriteInfo(item, false); + return { + text: insertText, + range: Range.fromPositions( + position.delta(0, -info.overwriteBefore), + position.delta(0, Math.max(info.overwriteAfter, 0)) + ), + }; +} diff --git a/src/vs/editor/contrib/inlineCompletions/utils.ts b/src/vs/editor/contrib/inlineCompletions/utils.ts new file mode 100644 index 0000000000..9923439d68 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/utils.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 { IDisposable, trackDisposable } from 'vs/base/common/lifecycle'; + +// TODO: merge this class into Matt's MutableDisposable. +/** + * Manages the lifecycle of a disposable value that may be changed. + * + * This ensures that when the disposable value is changed, the previously held disposable is disposed of. You can + * also register a `MutableDisposable` on a `Disposable` to ensure it is automatically cleaned up. + */ +export class MutableDisposable implements IDisposable { + private _value?: T; + private _isDisposed = false; + + constructor() { + trackDisposable(this); + } + + get value(): T | undefined { + return this._isDisposed ? undefined : this._value; + } + + set value(value: T | undefined) { + if (this._isDisposed || value === this._value) { + return; + } + + this._value?.dispose(); + this._value = value; + } + + clear() { + this.value = undefined; + } + + dispose(): void { + this._isDisposed = true; + this._value?.dispose(); + this._value = undefined; + } + + replace(newValue: T | undefined): T | undefined { + const oldValue = this._value; + this._value = newValue; + return oldValue; + } +} diff --git a/src/vs/editor/contrib/linesOperations/linesOperations.ts b/src/vs/editor/contrib/linesOperations/linesOperations.ts index 35f1b30bf7..efd2bef628 100644 --- a/src/vs/editor/contrib/linesOperations/linesOperations.ts +++ b/src/vs/editor/contrib/linesOperations/linesOperations.ts @@ -1045,7 +1045,41 @@ export class TitleCaseAction extends AbstractCaseAction { } } +class BackwardsCompatibleRegExp { + + private _actual: RegExp | null; + private _evaluated: boolean; + + constructor( + private readonly _pattern: string, + private readonly _flags: string + ) { + this._actual = null; + this._evaluated = false; + } + + public get(): RegExp | null { + if (!this._evaluated) { + this._evaluated = true; + try { + this._actual = new RegExp(this._pattern, this._flags); + } catch (err) { + // this browser does not support this regular expression + } + } + return this._actual; + } + + public isSupported(): boolean { + return (this.get() !== null); + } +} + export class SnakeCaseAction extends AbstractCaseAction { + + public static regExp1 = new BackwardsCompatibleRegExp('(\\p{Ll})(\\p{Lu})', 'gmu'); + public static regExp2 = new BackwardsCompatibleRegExp('(\\p{Lu}|\\p{N})(\\p{Lu})(\\p{Ll})', 'gmu'); + constructor() { super({ id: 'editor.action.transformToSnakecase', @@ -1056,9 +1090,15 @@ export class SnakeCaseAction extends AbstractCaseAction { } protected _modifyText(text: string, wordSeparators: string): string { + const regExp1 = SnakeCaseAction.regExp1.get(); + const regExp2 = SnakeCaseAction.regExp2.get(); + if (!regExp1 || !regExp2) { + // cannot support this + return text; + } return (text - .replace(/(\p{Ll})(\p{Lu})/gmu, '$1_$2') - .replace(/(\p{Lu}|\p{N})(\p{Lu})(\p{Ll})/gmu, '$1_$2$3') + .replace(regExp1, '$1_$2') + .replace(regExp2, '$1_$2$3') .toLocaleLowerCase() ); } @@ -1084,4 +1124,7 @@ registerEditorAction(TransposeAction); registerEditorAction(UpperCaseAction); registerEditorAction(LowerCaseAction); registerEditorAction(TitleCaseAction); -registerEditorAction(SnakeCaseAction); + +if (SnakeCaseAction.regExp1.isSupported() && SnakeCaseAction.regExp2.isSupported()) { + registerEditorAction(SnakeCaseAction); +} diff --git a/src/vs/editor/contrib/linkedEditing/linkedEditing.ts b/src/vs/editor/contrib/linkedEditing/linkedEditing.ts index e126cbed1f..b7b460d5c0 100644 --- a/src/vs/editor/contrib/linkedEditing/linkedEditing.ts +++ b/src/vs/editor/contrib/linkedEditing/linkedEditing.ts @@ -39,6 +39,7 @@ export class LinkedEditingContribution extends Disposable implements IEditorCont public static readonly ID = 'editor.contrib.linkedEditing'; private static readonly DECORATION = ModelDecorationOptions.register({ + description: 'linked-editing', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, className: DECORATION_CLASS_NAME }); diff --git a/src/vs/editor/contrib/links/links.ts b/src/vs/editor/contrib/links/links.ts index 4709435e60..a95977dfda 100644 --- a/src/vs/editor/contrib/links/links.ts +++ b/src/vs/editor/contrib/links/links.ts @@ -66,11 +66,13 @@ function getHoverMessage(link: Link, useMetaKey: boolean): MarkdownString { const decoration = { general: ModelDecorationOptions.register({ + description: 'detected-link', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, collapseOnReplaceEdit: true, inlineClassName: 'detected-link' }), active: ModelDecorationOptions.register({ + description: 'detected-link-active', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, collapseOnReplaceEdit: true, inlineClassName: 'detected-link-active' diff --git a/src/vs/editor/contrib/multicursor/multicursor.ts b/src/vs/editor/contrib/multicursor/multicursor.ts index 6c00a5bd07..2e175191ac 100644 --- a/src/vs/editor/contrib/multicursor/multicursor.ts +++ b/src/vs/editor/contrib/multicursor/multicursor.ts @@ -1047,6 +1047,7 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut } private static readonly _SELECTION_HIGHLIGHT_OVERVIEW = ModelDecorationOptions.register({ + description: 'selection-highlight-overview', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'selectionHighlight', overviewRuler: { @@ -1056,6 +1057,7 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut }); private static readonly _SELECTION_HIGHLIGHT = ModelDecorationOptions.register({ + description: 'selection-highlight', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'selectionHighlight', }); diff --git a/src/vs/editor/contrib/peekView/media/peekViewWidget.css b/src/vs/editor/contrib/peekView/media/peekViewWidget.css index 32636f6528..bd4d6d08d2 100644 --- a/src/vs/editor/contrib/peekView/media/peekViewWidget.css +++ b/src/vs/editor/contrib/peekView/media/peekViewWidget.css @@ -33,6 +33,7 @@ .monaco-editor .peekview-widget .head .peekview-title .filename { overflow: hidden; text-overflow: ellipsis; + white-space: nowrap; } .monaco-editor .peekview-widget .head .peekview-title .meta:not(:empty)::before { diff --git a/src/vs/editor/contrib/peekView/peekView.ts b/src/vs/editor/contrib/peekView/peekView.ts index c03cafc41f..f1f498f8dd 100644 --- a/src/vs/editor/contrib/peekView/peekView.ts +++ b/src/vs/editor/contrib/peekView/peekView.ts @@ -219,7 +219,7 @@ export abstract class PeekViewWidget extends ZoneWidget { setTitle(primaryHeading: string, secondaryHeading?: string): void { if (this._primaryHeading && this._secondaryHeading) { this._primaryHeading.innerText = primaryHeading; - this._primaryHeading.setAttribute('aria-label', primaryHeading); + this._primaryHeading.setAttribute('title', primaryHeading); if (secondaryHeading) { this._secondaryHeading.innerText = secondaryHeading; } else { diff --git a/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts b/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts index 13e84b0cf3..bc8f80f828 100644 --- a/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts @@ -8,7 +8,7 @@ import { IEditor } from 'vs/editor/common/editorCommon'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; 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 { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { stripIcons } from 'vs/base/common/iconLabels'; @@ -20,9 +20,9 @@ export abstract class AbstractEditorCommandsQuickAccessProvider extends Abstract keybindingService: IKeybindingService, commandService: ICommandService, telemetryService: ITelemetryService, - notificationService: INotificationService + dialogService: IDialogService ) { - super(options, instantiationService, keybindingService, commandService, telemetryService, notificationService); + super(options, instantiationService, keybindingService, commandService, telemetryService, dialogService); } /** diff --git a/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts b/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts index 19e01d1af1..cf68b41d3a 100644 --- a/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts @@ -195,6 +195,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu { range, options: { + description: 'quick-access-range-highlight', className: 'rangeHighlight', isWholeLine: true } @@ -204,6 +205,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu { range, options: { + description: 'quick-access-range-highlight-overview', overviewRuler: { color: themeColorFromId(overviewRulerRangeHighlight), position: OverviewRulerLane.Full diff --git a/src/vs/editor/contrib/snippet/snippetController2.ts b/src/vs/editor/contrib/snippet/snippetController2.ts index 90bb4f4100..fde537e9ba 100644 --- a/src/vs/editor/contrib/snippet/snippetController2.ts +++ b/src/vs/editor/contrib/snippet/snippetController2.ts @@ -43,7 +43,7 @@ const _defaultOptions: ISnippetInsertOptions = { export class SnippetController2 implements IEditorContribution { - public static ID = 'snippetController2'; + public static readonly ID = 'snippetController2'; static get(editor: ICodeEditor): SnippetController2 { return editor.getContribution(SnippetController2.ID); diff --git a/src/vs/editor/contrib/snippet/snippetParser.ts b/src/vs/editor/contrib/snippet/snippetParser.ts index 30cdb136d1..113e3a7219 100644 --- a/src/vs/editor/contrib/snippet/snippetParser.ts +++ b/src/vs/editor/contrib/snippet/snippetParser.ts @@ -388,11 +388,11 @@ export class FormatString extends Marker { } private _toPascalCase(value: string): string { - const match = value.match(/[a-z]+/gi); + const match = value.match(/[a-z0-9]+/gi); if (!match) { return value; } - return match.map(function (word) { + return match.map(word => { return word.charAt(0).toUpperCase() + word.substr(1).toLowerCase(); }) diff --git a/src/vs/editor/contrib/snippet/snippetSession.ts b/src/vs/editor/contrib/snippet/snippetSession.ts index 6b5af88988..0ec95eca75 100644 --- a/src/vs/editor/contrib/snippet/snippetSession.ts +++ b/src/vs/editor/contrib/snippet/snippetSession.ts @@ -45,10 +45,10 @@ export class OneSnippet { _nestingLevel: number = 1; private static readonly _decor = { - active: ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, className: 'snippet-placeholder' }), - inactive: ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'snippet-placeholder' }), - activeFinal: ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'finish-snippet-placeholder' }), - inactiveFinal: ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'finish-snippet-placeholder' }), + active: ModelDecorationOptions.register({ description: 'snippet-placeholder-1', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, className: 'snippet-placeholder' }), + inactive: ModelDecorationOptions.register({ description: 'snippet-placeholder-2', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'snippet-placeholder' }), + activeFinal: ModelDecorationOptions.register({ description: 'snippet-placeholder-3', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'finish-snippet-placeholder' }), + inactiveFinal: ModelDecorationOptions.register({ description: 'snippet-placeholder-4', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'finish-snippet-placeholder' }), }; constructor( diff --git a/src/vs/editor/contrib/snippet/test/snippetParser.test.ts b/src/vs/editor/contrib/snippet/test/snippetParser.test.ts index 82fd914eec..2994a01d07 100644 --- a/src/vs/editor/contrib/snippet/test/snippetParser.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetParser.test.ts @@ -655,6 +655,7 @@ suite('SnippetParser', () => { assert.strictEqual(new FormatString(1, 'capitalize').resolve('bar'), 'Bar'); assert.strictEqual(new FormatString(1, 'capitalize').resolve('bar no repeat'), 'Bar no repeat'); assert.strictEqual(new FormatString(1, 'pascalcase').resolve('bar-foo'), 'BarFoo'); + assert.strictEqual(new FormatString(1, 'pascalcase').resolve('bar-42-foo'), 'Bar42Foo'); assert.strictEqual(new FormatString(1, 'notKnown').resolve('input'), 'input'); // if diff --git a/src/vs/editor/contrib/suggest/media/suggest.css b/src/vs/editor/contrib/suggest/media/suggest.css index aced9375c7..ee663e6d84 100644 --- a/src/vs/editor/contrib/suggest/media/suggest.css +++ b/src/vs/editor/contrib/suggest/media/suggest.css @@ -306,6 +306,10 @@ margin-right: 4px; } +.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused .suggest-icon { + color: inherit; +} + .monaco-editor .suggest-widget.no-icons .monaco-list .monaco-list-row .icon, .monaco-editor .suggest-widget.no-icons .monaco-list .monaco-list-row .suggest-icon::before { display: none; } diff --git a/src/vs/editor/contrib/suggest/resizable.ts b/src/vs/editor/contrib/suggest/resizable.ts index 7b8d746938..bdba480a8f 100644 --- a/src/vs/editor/contrib/suggest/resizable.ts +++ b/src/vs/editor/contrib/suggest/resizable.ts @@ -121,6 +121,8 @@ export class ResizableHTMLElement { this._eastSash.dispose(); this._westSash.dispose(); this._sashListener.dispose(); + this._onDidResize.dispose(); + this._onDidWillResize.dispose(); this.domNode.remove(); } diff --git a/src/vs/editor/contrib/suggest/suggest.ts b/src/vs/editor/contrib/suggest/suggest.ts index 5434651989..c691906375 100644 --- a/src/vs/editor/contrib/suggest/suggest.ts +++ b/src/vs/editor/contrib/suggest/suggest.ts @@ -155,6 +155,7 @@ export class CompletionOptions { readonly snippetSortOrder = SnippetSortOrder.Bottom, readonly kindFilter = new Set(), readonly providerFilter = new Set(), + readonly showDeprecated = true ) { } } @@ -216,6 +217,10 @@ export async function provideSuggestionItems( } for (let suggestion of container.suggestions) { if (!options.kindFilter.has(suggestion.kind)) { + // skip if not showing deprecated suggestions + if (!options.showDeprecated && suggestion?.tags?.includes(modes.CompletionItemTag.Deprecated)) { + continue; + } // fill in default range when missing if (!suggestion.range) { suggestion.range = defaultRange; diff --git a/src/vs/editor/contrib/suggest/suggestController.ts b/src/vs/editor/contrib/suggest/suggestController.ts index f148b902ad..9db46b28a2 100644 --- a/src/vs/editor/contrib/suggest/suggestController.ts +++ b/src/vs/editor/contrib/suggest/suggestController.ts @@ -61,7 +61,7 @@ class LineSuffix { const end = _model.getPositionAt(offset + 1); this._marker = _model.deltaDecorations([], [{ range: Range.fromPositions(_position, end), - options: { stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } + options: { description: 'suggest-line-suffix', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } }]); } } @@ -587,6 +587,14 @@ export class SuggestController implements IEditorContribution { resetWidgetSize(): void { this.widget.value.resetPersistedSize(); } + + forceRenderingAbove() { + this.widget.value.forceRenderingAbove(); + } + + stopForceRenderingAbove() { + this.widget.value.stopForceRenderingAbove(); + } } export class TriggerSuggestAction extends EditorAction { diff --git a/src/vs/editor/contrib/suggest/suggestModel.ts b/src/vs/editor/contrib/suggest/suggestModel.ts index 57f62d1499..cb122d6cf1 100644 --- a/src/vs/editor/contrib/suggest/suggestModel.ts +++ b/src/vs/editor/contrib/suggest/suggestModel.ts @@ -428,13 +428,13 @@ export class SuggestModel implements IDisposable { break; } - const itemKindFilter = SuggestModel._createItemKindFilter(this._editor); + const { itemKind: itemKindFilter, showDeprecated } = SuggestModel._createSuggestFilter(this._editor); const wordDistance = WordDistance.create(this._editorWorkerService, this._editor); const completions = provideSuggestionItems( model, this._editor.getPosition(), - new CompletionOptions(snippetSortOrder, itemKindFilter, onlyFrom), + new CompletionOptions(snippetSortOrder, itemKindFilter, onlyFrom, showDeprecated), suggestCtx, this._requestToken.token ); @@ -502,7 +502,7 @@ export class SuggestModel implements IDisposable { }); } - private static _createItemKindFilter(editor: ICodeEditor): Set { + private static _createSuggestFilter(editor: ICodeEditor): { itemKind: Set; showDeprecated: boolean } { // kind filter and snippet sort rules const result = new Set(); @@ -543,7 +543,7 @@ export class SuggestModel implements IDisposable { if (!suggestOptions.showUsers) { result.add(CompletionItemKind.User); } if (!suggestOptions.showIssues) { result.add(CompletionItemKind.Issue); } - return result; + return { itemKind: result, showDeprecated: suggestOptions.showDeprecated }; } private _onNewContext(ctx: LineContext): void { diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 2d87399c7e..c49afc1dd9 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -21,7 +21,7 @@ import { Context as SuggestContext, CompletionItem } from './suggest'; import { CompletionModel } from './completionModel'; import { attachListStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, IColorTheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { registerColor, editorWidgetBackground, quickInputListFocusBackground, activeContrastBorder, listHighlightForeground, editorForeground, editorWidgetBorder, focusBorder, textLinkForeground, textCodeBlockBackground } from 'vs/platform/theme/common/colorRegistry'; +import { registerColor, editorWidgetBackground, quickInputListFocusBackground, activeContrastBorder, listHighlightForeground, editorForeground, editorWidgetBorder, focusBorder, textLinkForeground, textCodeBlockBackground, quickInputListFocusForeground, listFocusHighlightForeground } from 'vs/platform/theme/common/colorRegistry'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { TimeoutTimer, CancelablePromise, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -40,8 +40,10 @@ import { clamp } from 'vs/base/common/numbers'; export const editorSuggestWidgetBackground = registerColor('editorSuggestWidget.background', { dark: editorWidgetBackground, light: editorWidgetBackground, hc: editorWidgetBackground }, nls.localize('editorSuggestWidgetBackground', 'Background color of the suggest widget.')); export const editorSuggestWidgetBorder = registerColor('editorSuggestWidget.border', { dark: editorWidgetBorder, light: editorWidgetBorder, hc: editorWidgetBorder }, nls.localize('editorSuggestWidgetBorder', 'Border color of the suggest widget.')); export const editorSuggestWidgetForeground = registerColor('editorSuggestWidget.foreground', { dark: editorForeground, light: editorForeground, hc: editorForeground }, nls.localize('editorSuggestWidgetForeground', 'Foreground color of the suggest widget.')); +export const editorSuggestWidgetSelectedForeground = registerColor('editorSuggestWidget.selectedForeground', { dark: quickInputListFocusForeground, light: quickInputListFocusForeground, hc: quickInputListFocusForeground }, nls.localize('editorSuggestWidgetSelectedForeground', 'Foreground color of the selected entry in the suggest widget.')); export const editorSuggestWidgetSelectedBackground = registerColor('editorSuggestWidget.selectedBackground', { dark: quickInputListFocusBackground, light: quickInputListFocusBackground, hc: quickInputListFocusBackground }, nls.localize('editorSuggestWidgetSelectedBackground', 'Background color of the selected entry in the suggest widget.')); export const editorSuggestWidgetHighlightForeground = registerColor('editorSuggestWidget.highlightForeground', { dark: listHighlightForeground, light: listHighlightForeground, hc: listHighlightForeground }, nls.localize('editorSuggestWidgetHighlightForeground', 'Color of the match highlights in the suggest widget.')); +export const editorSuggestWidgetHighlightFocusForeground = registerColor('editorSuggestWidget.focusHighlightForeground', { dark: listFocusHighlightForeground, light: listFocusHighlightForeground, hc: listFocusHighlightForeground }, nls.localize('editorSuggestWidgetFocusHighlightForeground', 'Color of the match highlights in the suggest widget when an item is focused.')); const enum State { Hidden, @@ -104,6 +106,7 @@ export class SuggestWidget implements IDisposable { private _ignoreFocusEvents: boolean = false; private _completionModel?: CompletionModel; private _cappedHeight?: { wanted: number; capped: number; }; + private _forceRenderingAbove: boolean = false; private _explainMode: boolean = false; readonly element: ResizableHTMLElement; @@ -804,7 +807,7 @@ export class SuggestWidget implements IDisposable { height = maxHeight; } - if (height > maxHeightBelow) { + if (height > maxHeightBelow || this._forceRenderingAbove) { this._contentWidget.setPreference(ContentWidgetPositionPreference.ABOVE); this.element.enableSashes(true, true, false, false); maxHeight = maxHeightAbove; @@ -875,6 +878,17 @@ export class SuggestWidget implements IDisposable { private _setDetailsVisible(value: boolean) { this._storageService.store('expandSuggestionDocs', value, StorageScope.GLOBAL, StorageTarget.USER); } + + forceRenderingAbove() { + if (!this._forceRenderingAbove) { + this._forceRenderingAbove = true; + this._layout(this._persistedSize.restore()); + } + } + + stopForceRenderingAbove() { + this._forceRenderingAbove = false; + } } export class SuggestContentWidget implements IContentWidget { @@ -972,11 +986,22 @@ registerThemingParticipant((theme, collector) => { if (matchHighlight) { collector.addRule(`.monaco-editor .suggest-widget .monaco-list .monaco-list-row .monaco-highlighted-label .highlight { color: ${matchHighlight}; }`); } + + const matchHighlightFocus = theme.getColor(editorSuggestWidgetHighlightFocusForeground); + if (matchHighlight) { + collector.addRule(`.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused .monaco-highlighted-label .highlight { color: ${matchHighlightFocus}; }`); + } + const foreground = theme.getColor(editorSuggestWidgetForeground); if (foreground) { collector.addRule(`.monaco-editor .suggest-widget, .monaco-editor .suggest-details { color: ${foreground}; }`); } + const selectedForeground = theme.getColor(editorSuggestWidgetSelectedForeground); + if (selectedForeground) { + collector.addRule(`.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused { color: ${selectedForeground}; }`); + } + const link = theme.getColor(textLinkForeground); if (link) { collector.addRule(`.monaco-editor .suggest-details a { color: ${link}; }`); diff --git a/src/vs/editor/contrib/suggest/suggestWidgetDetails.ts b/src/vs/editor/contrib/suggest/suggestWidgetDetails.ts index 239f1669cb..5a60a0ffe0 100644 --- a/src/vs/editor/contrib/suggest/suggestWidgetDetails.ts +++ b/src/vs/editor/contrib/suggest/suggestWidgetDetails.ts @@ -320,6 +320,7 @@ export class SuggestDetailsOverlay implements IOverlayWidget { } dispose(): void { + this._resizable.dispose(); this._disposables.dispose(); this.hide(); } diff --git a/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts b/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts index b6f8298073..7eac235025 100644 --- a/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts +++ b/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts @@ -209,13 +209,13 @@ export class ItemRenderer implements IListRenderer this.update())); this.update(); } diff --git a/src/vs/editor/standalone/browser/quickAccess/standaloneCommandsQuickAccess.ts b/src/vs/editor/standalone/browser/quickAccess/standaloneCommandsQuickAccess.ts index baa89f0a48..273d1a8a08 100644 --- a/src/vs/editor/standalone/browser/quickAccess/standaloneCommandsQuickAccess.ts +++ b/src/vs/editor/standalone/browser/quickAccess/standaloneCommandsQuickAccess.ts @@ -15,7 +15,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { EditorAction, registerEditorAction } from 'vs/editor/browser/editorExtensions'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -32,9 +32,9 @@ export class StandaloneCommandsQuickAccessProvider extends AbstractEditorCommand @IKeybindingService keybindingService: IKeybindingService, @ICommandService commandService: ICommandService, @ITelemetryService telemetryService: ITelemetryService, - @INotificationService notificationService: INotificationService + @IDialogService dialogService: IDialogService ) { - super({ showAlias: false }, instantiationService, keybindingService, commandService, telemetryService, notificationService); + super({ showAlias: false }, instantiationService, keybindingService, commandService, telemetryService, dialogService); } protected async getCommandPicks(): Promise> { diff --git a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInput.css b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInput.css index 87cfdb4259..a4a19dc775 100644 --- a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInput.css +++ b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInput.css @@ -12,6 +12,11 @@ color: #0066BF; } +.vs .quick-input-widget .monaco-list-row.focused .monaco-highlighted-label .highlight, +.vs .quick-input-widget .monaco-list-row.focused .monaco-highlighted-label .highlight { + color: #9DDDFF; +} + .vs-dark .quick-input-widget .monaco-highlighted-label .highlight, .vs-dark .quick-input-widget .monaco-highlighted-label .highlight { color: #0097fb; diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index f06cce00dd..b92a829a7c 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -34,6 +34,7 @@ import { StandaloneThemeServiceImpl } from 'vs/editor/standalone/browser/standal import { IModelService } from 'vs/editor/common/services/modelService'; import { ILanguageSelection, IModeService } from 'vs/editor/common/services/modeService'; import { URI } from 'vs/base/common/uri'; +import { StandaloneCodeEditorServiceImpl } from 'vs/editor/standalone/browser/standaloneCodeServiceImpl'; /** * Description of an action contribution @@ -363,6 +364,20 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon return toDispose; } + + protected override _triggerCommand(handlerId: string, payload: any): void { + if (this._codeEditorService instanceof StandaloneCodeEditorServiceImpl) { + // Help commands find this editor as the active editor + try { + this._codeEditorService.setActiveCodeEditor(this); + super._triggerCommand(handlerId, payload); + } finally { + this._codeEditorService.setActiveCodeEditor(null); + } + } else { + super._triggerCommand(handlerId, payload); + } + } } export class StandaloneEditor extends StandaloneCodeEditor implements IStandaloneCodeEditor { diff --git a/src/vs/editor/standalone/browser/standaloneCodeServiceImpl.ts b/src/vs/editor/standalone/browser/standaloneCodeServiceImpl.ts index 7570f31340..b0095447ce 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeServiceImpl.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeServiceImpl.ts @@ -12,12 +12,13 @@ import { IRange } from 'vs/editor/common/core/range'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { IResourceEditorInput, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IThemeService } from 'vs/platform/theme/common/themeService'; export class StandaloneCodeEditorServiceImpl extends CodeEditorServiceImpl { private readonly _editorIsOpen: IContextKey; + private _activeCodeEditor: ICodeEditor | null; constructor( styleSheet: GlobalStyleSheet | null, @@ -28,6 +29,7 @@ export class StandaloneCodeEditorServiceImpl extends CodeEditorServiceImpl { this.onCodeEditorAdd(() => this._checkContextKey()); this.onCodeEditorRemove(() => this._checkContextKey()); this._editorIsOpen = contextKeyService.createKey('editorIsOpen', false); + this._activeCodeEditor = null; } private _checkContextKey(): void { @@ -41,8 +43,12 @@ export class StandaloneCodeEditorServiceImpl extends CodeEditorServiceImpl { this._editorIsOpen.set(hasCodeEditor); } + public setActiveCodeEditor(activeCodeEditor: ICodeEditor | null): void { + this._activeCodeEditor = activeCodeEditor; + } + public getActiveCodeEditor(): ICodeEditor | null { - return null; // not supported in the standalone case + return this._activeCodeEditor; } public openCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise { @@ -53,7 +59,7 @@ export class StandaloneCodeEditorServiceImpl extends CodeEditorServiceImpl { return Promise.resolve(this.doOpenEditor(source, input)); } - private doOpenEditor(editor: ICodeEditor, input: IResourceEditorInput): ICodeEditor | null { + private doOpenEditor(editor: ICodeEditor, input: ITextResourceEditorInput): ICodeEditor | null { const model = this.findModel(editor, input.resource); if (!model) { if (input.resource) { diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index 7292c4f447..5f9082f246 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -547,6 +547,13 @@ export function registerDocumentRangeSemanticTokensProvider(languageId: string, return modes.DocumentRangeSemanticTokensProviderRegistry.register(languageId, provider); } +/** + * Register an inline completions provider. + */ +export function registerInlineCompletionsProvider(languageId: string, provider: modes.InlineCompletionsProvider): IDisposable { + return modes.InlineCompletionsProviderRegistry.register(languageId, provider); +} + /** * Contains additional diagnostic information about the context in which * a [code action](#CodeActionProvider.provideCodeActions) is run. @@ -613,6 +620,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { registerSelectionRangeProvider: registerSelectionRangeProvider, registerDocumentSemanticTokensProvider: registerDocumentSemanticTokensProvider, registerDocumentRangeSemanticTokensProvider: registerDocumentRangeSemanticTokensProvider, + registerInlineCompletionsProvider: registerInlineCompletionsProvider, // enums DocumentHighlightKind: standaloneEnums.DocumentHighlightKind, @@ -624,7 +632,8 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { IndentAction: standaloneEnums.IndentAction, CompletionTriggerKind: standaloneEnums.CompletionTriggerKind, SignatureHelpTriggerKind: standaloneEnums.SignatureHelpTriggerKind, - InlineHintKind: standaloneEnums.InlineHintKind, + InlayHintKind: standaloneEnums.InlayHintKind, + InlineCompletionTriggerKind: standaloneEnums.InlineCompletionTriggerKind, // classes FoldingRangeKind: modes.FoldingRangeKind, diff --git a/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts b/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts index 6cb30e83e9..e7c23b02e9 100644 --- a/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts +++ b/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts @@ -233,7 +233,7 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon this._updateCSS(); }); - window.matchMedia('(forced-colors: active)').addEventListener('change', () => { + dom.addMatchMediaChangeListener('(forced-colors: active)', () => { this._updateActualTheme(); }); } diff --git a/src/vs/editor/standalone/common/monarch/monarchCompile.ts b/src/vs/editor/standalone/common/monarch/monarchCompile.ts index 8ee8458d66..2bdc50cdfe 100644 --- a/src/vs/editor/standalone/common/monarch/monarchCompile.ts +++ b/src/vs/editor/standalone/common/monarch/monarchCompile.ts @@ -86,15 +86,14 @@ function createKeywordMatcher(arr: string[], caseInsensitive: boolean = false): * @example /@@text/ will not be replaced and will become /@text/. */ function compileRegExp(lexer: monarchCommon.ILexerMin, str: string): RegExp { + // @@ must be interpreted as a literal @, so we replace all occurences of @@ with a placeholder character + str = str.replace(/@@/g, `\x01`); + let n = 0; let hadExpansion: boolean; do { hadExpansion = false; - str = str.replace(/(.|^)@(\w+)/g, function (s, charBeforeAtSign, attr?) { - if (charBeforeAtSign === '@') { - // do not expand @@ - return s; - } + str = str.replace(/@(\w+)/g, function (s, attr?) { hadExpansion = true; let sub = ''; if (typeof (lexer[attr]) === 'string') { @@ -108,13 +107,13 @@ function compileRegExp(lexer: monarchCommon.ILexerMin, str: string): RegExp { throw monarchCommon.createError(lexer, 'attribute reference \'' + attr + '\' must be a string, used at: ' + str); } } - return charBeforeAtSign + (monarchCommon.empty(sub) ? '' : '(?:' + sub + ')'); + return (monarchCommon.empty(sub) ? '' : '(?:' + sub + ')'); }); n++; } while (hadExpansion && n < 5); // handle escaped @@ - str = str.replace(/@@/g, '@'); + str = str.replace(/\x01/g, '@'); let flags = (lexer.ignoreCase ? 'i' : '') + (lexer.unicode ? 'u' : ''); return new RegExp(str, flags); diff --git a/src/vs/editor/standalone/common/themes.ts b/src/vs/editor/standalone/common/themes.ts index b9c4885b03..42d16f50dc 100644 --- a/src/vs/editor/standalone/common/themes.ts +++ b/src/vs/editor/standalone/common/themes.ts @@ -5,7 +5,7 @@ import { editorActiveIndentGuides, editorIndentGuides } from 'vs/editor/common/view/editorColorRegistry'; import { IStandaloneThemeData } from 'vs/editor/standalone/common/standaloneThemeService'; -import { editorBackground, editorForeground, editorInactiveSelection, editorSelectionHighlight } from 'vs/platform/theme/common/colorRegistry'; +import { editorBackground, editorForeground, editorInactiveSelection, editorSelectionHighlight, listFocusHighlightForeground } from 'vs/platform/theme/common/colorRegistry'; /* -------------------------------- Begin vs theme -------------------------------- */ export const vs: IStandaloneThemeData = { @@ -73,7 +73,8 @@ export const vs: IStandaloneThemeData = { [editorInactiveSelection]: '#E5EBF1', [editorIndentGuides]: '#D3D3D3', [editorActiveIndentGuides]: '#939393', - [editorSelectionHighlight]: '#ADD6FF4D' + [editorSelectionHighlight]: '#ADD6FF4D', + [listFocusHighlightForeground]: '#9DDDFF' } }; /* -------------------------------- End vs theme -------------------------------- */ diff --git a/src/vs/editor/standalone/test/monarch/monarch.test.ts b/src/vs/editor/standalone/test/monarch/monarch.test.ts index bcc7a089b1..5dc54173a9 100644 --- a/src/vs/editor/standalone/test/monarch/monarch.test.ts +++ b/src/vs/editor/standalone/test/monarch/monarch.test.ts @@ -264,4 +264,31 @@ suite('Monarch', () => { ]); }); + test('microsoft/monaco-editor#2424: Allow to target @@', () => { + const modeService = new ModeServiceImpl(); + + const tokenizer = createMonarchTokenizer(modeService, 'test', { + ignoreCase: false, + tokenizer: { + root: [ + { + regex: /@@@@/, + action: { token: 'ham' } + }, + ], + }, + }); + + const lines = [ + `@@` + ]; + + const actualTokens = getTokens(tokenizer, lines); + assert.deepStrictEqual(actualTokens, [ + [ + new Token(0, 'ham.test', 'test'), + ] + ]); + }); + }); diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index 6ba25f1066..4e50a64d98 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -2608,6 +2608,186 @@ suite('Editor Controller - Regression tests', () => { model.dispose(); }); + + test('issue #122914: Left delete behavior in some languages is changed (useTabStops: false)', () => { + let model = createTextModel( + [ + 'สวัสดี' + ].join('\n') + ); + + withTestCodeEditor(null, { model: model, useTabStops: false }, (editor, viewModel) => { + editor.setSelections([ + new Selection(1, 7, 1, 7) + ]); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'สวัสด'); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'สวัส'); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'สวั'); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'สว'); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'ส'); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), ''); + }); + + model.dispose(); + }); + + test('issue #99629: Emoji modifiers in text treated separately when using backspace', () => { + const model = createTextModel( + [ + '👶🏾' + ].join('\n') + ); + + withTestCodeEditor(null, { model: model, useTabStops: false }, (editor, viewModel) => { + const len = model.getValueLength(); + editor.setSelections([ + new Selection(1, 1 + len, 1, 1 + len) + ]); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), ''); + }); + + model.dispose(); + }); + + test('issue #99629: Emoji modifiers in text treated separately when using backspace (ZWJ sequence)', () => { + let model = createTextModel( + [ + '👨‍👩🏽‍👧‍👦' + ].join('\n') + ); + + withTestCodeEditor(null, { model: model, useTabStops: false }, (editor, viewModel) => { + const len = model.getValueLength(); + editor.setSelections([ + new Selection(1, 1 + len, 1, 1 + len) + ]); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '👨‍👩🏽‍👧'); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '👨‍👩🏽'); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '👨'); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), ''); + }); + + model.dispose(); + }); + + test('issue #105730: move left behaves differently for multiple cursors', () => { + const model = createTextModel('asdfghjkl, asdfghjkl, asdfghjkl, '); + + withTestCodeEditor( + null, + { + model: model, + wordWrap: 'wordWrapColumn', + wordWrapColumn: 24 + }, + (editor, viewModel) => { + viewModel.setSelections('test', [ + new Selection(1, 10, 1, 12), + new Selection(1, 21, 1, 23), + new Selection(1, 32, 1, 34) + ]); + moveLeft(editor, viewModel, false); + assertCursor(viewModel, [ + new Selection(1, 10, 1, 10), + new Selection(1, 21, 1, 21), + new Selection(1, 32, 1, 32) + ]); + + viewModel.setSelections('test', [ + new Selection(1, 10, 1, 12), + new Selection(1, 21, 1, 23), + new Selection(1, 32, 1, 34) + ]); + moveLeft(editor, viewModel, true); + assertCursor(viewModel, [ + new Selection(1, 10, 1, 11), + new Selection(1, 21, 1, 22), + new Selection(1, 32, 1, 33) + ]); + }); + }); + + test('issue #105730: move right should always skip wrap point', () => { + const model = createTextModel('asdfghjkl, asdfghjkl, asdfghjkl, \nasdfghjkl,'); + + withTestCodeEditor( + null, + { + model: model, + wordWrap: 'wordWrapColumn', + wordWrapColumn: 24 + }, + (editor, viewModel) => { + viewModel.setSelections('test', [ + new Selection(1, 22, 1, 22) + ]); + moveRight(editor, viewModel, false); + moveRight(editor, viewModel, false); + assertCursor(viewModel, [ + new Selection(1, 24, 1, 24), + ]); + + viewModel.setSelections('test', [ + new Selection(1, 22, 1, 22) + ]); + moveRight(editor, viewModel, true); + moveRight(editor, viewModel, true); + assertCursor(viewModel, [ + new Selection(1, 22, 1, 24), + ]); + } + ); + }); + + test('issue #123178: sticky tab in consecutive wrapped lines', () => { + const model = createTextModel(' aaaa aaaa', { tabSize: 4 }); + + withTestCodeEditor( + null, + { + model: model, + wordWrap: 'wordWrapColumn', + wordWrapColumn: 8, + stickyTabStops: true, + }, + (editor, viewModel) => { + viewModel.setSelections('test', [ + new Selection(1, 9, 1, 9) + ]); + moveRight(editor, viewModel, false); + assertCursor(viewModel, [ + new Selection(1, 10, 1, 10), + ]); + + moveLeft(editor, viewModel, false); + assertCursor(viewModel, [ + new Selection(1, 9, 1, 9), + ]); + } + ); + }); }); suite('Editor Controller - Cursor Configuration', () => { @@ -4299,6 +4479,29 @@ suite('Editor Controller - Indentation Rules', () => { assert.strictEqual(model.getValue(), ' let a,\n\t b,\n\t c;'); }); }); + + test('issue #122714: tabSize=1 prevent typing a string matching decreaseIndentPattern in an empty file', () => { + let latexMode = new IndentRulesMode({ + increaseIndentPattern: new RegExp('\\\\begin{(?!document)([^}]*)}(?!.*\\\\end{\\1})'), + decreaseIndentPattern: new RegExp('^\\s*\\\\end{(?!document)') + }); + let model = createTextModel( + '\\end', + { tabSize: 1 }, + latexMode.getLanguageIdentifier() + ); + + withTestCodeEditor(null, { model: model, autoIndent: 'full' }, (editor, viewModel) => { + moveTo(editor, viewModel, 1, 5, false); + assertCursor(viewModel, new Selection(1, 5, 1, 5)); + + viewModel.type('{', 'keyboard'); + assert.strictEqual(model.getLineContent(1), '\\end{}'); + }); + + latexMode.dispose(); + model.dispose(); + }); }); interface ICursorOpts { @@ -6080,4 +6283,62 @@ suite('Undo stops', () => { assert.strictEqual(model.getValue(), 'hello world\nhello world'); }); }); + + test('there is a single undo stop for consecutive whitespaces', () => { + let model = createTextModel( + [ + '' + ].join('\n'), + { + insertSpaces: false, + } + ); + + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + viewModel.type('a', 'keyboard'); + viewModel.type('b', 'keyboard'); + viewModel.type(' ', 'keyboard'); + viewModel.type(' ', 'keyboard'); + viewModel.type('c', 'keyboard'); + viewModel.type('d', 'keyboard'); + + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'ab cd', 'assert1'); + + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'ab ', 'assert2'); + + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'ab', 'assert3'); + + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '', 'assert4'); + }); + }); + + test('there is no undo stop after a single whitespace', () => { + let model = createTextModel( + [ + '' + ].join('\n'), + { + insertSpaces: false, + } + ); + + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + viewModel.type('a', 'keyboard'); + viewModel.type('b', 'keyboard'); + viewModel.type(' ', 'keyboard'); + viewModel.type('c', 'keyboard'); + viewModel.type('d', 'keyboard'); + + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'ab cd', 'assert1'); + + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'ab', 'assert3'); + + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '', 'assert4'); + }); + }); }); diff --git a/src/vs/editor/test/browser/editorTestServices.ts b/src/vs/editor/test/browser/editorTestServices.ts index 40e3e6e8d5..1178a0e146 100644 --- a/src/vs/editor/test/browser/editorTestServices.ts +++ b/src/vs/editor/test/browser/editorTestServices.ts @@ -19,9 +19,9 @@ export class TestCodeEditorService extends AbstractCodeEditorService { this.lastInput = input; return Promise.resolve(null); } - public registerDecorationType(key: string, options: IDecorationRenderOptions, parentTypeKey?: string): void { } + public registerDecorationType(description: string, key: string, options: IDecorationRenderOptions, parentTypeKey?: string): void { } public removeDecorationType(key: string): void { } - public resolveDecorationOptions(decorationTypeKey: string, writable: boolean): IModelDecorationOptions { return {}; } + public resolveDecorationOptions(decorationTypeKey: string, writable: boolean): IModelDecorationOptions { return { description: 'test' }; } public resolveDecorationCSSRules(decorationTypeKey: string): CSSRuleList | null { return null; } } diff --git a/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts b/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts index 38f89afde0..2d9e7410b5 100644 --- a/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts +++ b/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts @@ -59,12 +59,12 @@ suite.skip('Decoration Render Options', () => { // {{SQL CARBON EDIT}} skip suit }; test('register and resolve decoration type', () => { let s = new TestCodeEditorServiceImpl(null, themeServiceMock); - s.registerDecorationType('example', options); + s.registerDecorationType('test', 'example', options); assert.notStrictEqual(s.resolveDecorationOptions('example', false), undefined); }); test('remove decoration type', () => { let s = new TestCodeEditorServiceImpl(null, themeServiceMock); - s.registerDecorationType('example', options); + s.registerDecorationType('test', 'example', options); assert.notStrictEqual(s.resolveDecorationOptions('example', false), undefined); s.removeDecorationType('example'); assert.throws(() => s.resolveDecorationOptions('example', false)); @@ -77,7 +77,7 @@ suite.skip('Decoration Render Options', () => { // {{SQL CARBON EDIT}} skip suit test('css properties', () => { const styleSheet = new TestGlobalStyleSheet(); const s = new TestCodeEditorServiceImpl(styleSheet, themeServiceMock); - s.registerDecorationType('example', options); + s.registerDecorationType('test', 'example', options); const sheet = readStyleSheet(styleSheet); assert(sheet.indexOf(`{background:url('https://github.com/microsoft/vscode/blob/main/resources/linux/code.png') center center no-repeat;background-size:contain;}`) >= 0); assert(sheet.indexOf(`{background-color:red;border-color:yellow;box-sizing: border-box;}`) >= 0); @@ -94,7 +94,7 @@ suite.skip('Decoration Render Options', () => { // {{SQL CARBON EDIT}} skip suit editorBackground: '#FF0000' })); const s = new TestCodeEditorServiceImpl(styleSheet, themeService); - s.registerDecorationType('example', options); + s.registerDecorationType('test', 'example', options); assert.strictEqual(readStyleSheet(styleSheet), '.monaco-editor .ced-example-0 {background-color:#ff0000;border-color:transparent;box-sizing: border-box;}'); themeService.setTheme(new TestColorTheme({ @@ -127,7 +127,7 @@ suite.skip('Decoration Render Options', () => { // {{SQL CARBON EDIT}} skip suit infoForeground: '#444444' })); const s = new TestCodeEditorServiceImpl(styleSheet, themeService); - s.registerDecorationType('example', options); + s.registerDecorationType('test', 'example', options); const expected = [ '.vs-dark.monaco-editor .ced-example-4::after, .hc-black.monaco-editor .ced-example-4::after {color:#444444 !important;}', '.vs-dark.monaco-editor .ced-example-1, .hc-black.monaco-editor .ced-example-1 {color:#000000 !important;}', @@ -145,7 +145,7 @@ suite.skip('Decoration Render Options', () => { // {{SQL CARBON EDIT}} skip suit const s = new TestCodeEditorServiceImpl(styleSheet, themeServiceMock); // URI, only minimal encoding - s.registerDecorationType('example', { gutterIconPath: URI.parse('data:image/svg+xml;base64,PHN2ZyB4b+') }); + s.registerDecorationType('test', 'example', { gutterIconPath: URI.parse('data:image/svg+xml;base64,PHN2ZyB4b+') }); assert(readStyleSheet(styleSheet).indexOf(`{background:url('data:image/svg+xml;base64,PHN2ZyB4b+') center center no-repeat;}`) > 0); s.removeDecorationType('example'); @@ -159,27 +159,27 @@ suite.skip('Decoration Render Options', () => { // {{SQL CARBON EDIT}} skip suit if (platform.isWindows) { // windows file path (used as string) - s.registerDecorationType('example', { gutterIconPath: URI.file('c:\\files\\miles\\more.png') }); + s.registerDecorationType('test', 'example', { gutterIconPath: URI.file('c:\\files\\miles\\more.png') }); assertBackground('file:///c:/files/miles/more.png', 'vscode-file://vscode-app/c:/files/miles/more.png'); s.removeDecorationType('example'); // single quote must always be escaped/encoded - s.registerDecorationType('example', { gutterIconPath: URI.file('c:\\files\\foo\\b\'ar.png') }); + s.registerDecorationType('test', 'example', { gutterIconPath: URI.file('c:\\files\\foo\\b\'ar.png') }); assertBackground('file:///c:/files/foo/b%27ar.png', 'vscode-file://vscode-app/c:/files/foo/b%27ar.png'); s.removeDecorationType('example'); } else { // unix file path (used as string) - s.registerDecorationType('example', { gutterIconPath: URI.file('/Users/foo/bar.png') }); + s.registerDecorationType('test', 'example', { gutterIconPath: URI.file('/Users/foo/bar.png') }); assertBackground('file:///Users/foo/bar.png', 'vscode-file://vscode-app/Users/foo/bar.png'); s.removeDecorationType('example'); // single quote must always be escaped/encoded - s.registerDecorationType('example', { gutterIconPath: URI.file('/Users/foo/b\'ar.png') }); + s.registerDecorationType('test', 'example', { gutterIconPath: URI.file('/Users/foo/b\'ar.png') }); assertBackground('file:///Users/foo/b%27ar.png', 'vscode-file://vscode-app/Users/foo/b%27ar.png'); s.removeDecorationType('example'); } - s.registerDecorationType('example', { gutterIconPath: URI.parse('http://test/pa\'th') }); + s.registerDecorationType('test', 'example', { gutterIconPath: URI.parse('http://test/pa\'th') }); assert(readStyleSheet(styleSheet).indexOf(`{background:url('http://test/pa%27th') center center no-repeat;}`) > 0); s.removeDecorationType('example'); }); diff --git a/src/vs/editor/test/browser/services/openerService.test.ts b/src/vs/editor/test/browser/services/openerService.test.ts index 8ed2e8ee38..28788bd591 100644 --- a/src/vs/editor/test/browser/services/openerService.test.ts +++ b/src/vs/editor/test/browser/services/openerService.test.ts @@ -8,6 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { OpenerService } from 'vs/editor/browser/services/openerService'; import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices'; import { CommandsRegistry, ICommandService, NullCommandService } from 'vs/platform/commands/common/commands'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { matchesScheme } from 'vs/platform/opener/common/opener'; suite('OpenerService', function () { @@ -32,28 +33,28 @@ suite('OpenerService', function () { test('delegate to editorService, scheme:///fff', async function () { const openerService = new OpenerService(editorService, NullCommandService); await openerService.open(URI.parse('another:///somepath')); - assert.strictEqual(editorService.lastInput!.options!.selection, undefined); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection, undefined); }); test('delegate to editorService, scheme:///fff#L123', async function () { const openerService = new OpenerService(editorService, NullCommandService); await openerService.open(URI.parse('file:///somepath#L23')); - assert.strictEqual(editorService.lastInput!.options!.selection!.startLineNumber, 23); - assert.strictEqual(editorService.lastInput!.options!.selection!.startColumn, 1); - assert.strictEqual(editorService.lastInput!.options!.selection!.endLineNumber, undefined); - assert.strictEqual(editorService.lastInput!.options!.selection!.endColumn, undefined); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.startLineNumber, 23); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.startColumn, 1); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.endLineNumber, undefined); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.endColumn, undefined); assert.strictEqual(editorService.lastInput!.resource.fragment, ''); await openerService.open(URI.parse('another:///somepath#L23')); - assert.strictEqual(editorService.lastInput!.options!.selection!.startLineNumber, 23); - assert.strictEqual(editorService.lastInput!.options!.selection!.startColumn, 1); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.startLineNumber, 23); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.startColumn, 1); await openerService.open(URI.parse('another:///somepath#L23,45')); - assert.strictEqual(editorService.lastInput!.options!.selection!.startLineNumber, 23); - assert.strictEqual(editorService.lastInput!.options!.selection!.startColumn, 45); - assert.strictEqual(editorService.lastInput!.options!.selection!.endLineNumber, undefined); - assert.strictEqual(editorService.lastInput!.options!.selection!.endColumn, undefined); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.startLineNumber, 23); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.startColumn, 45); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.endLineNumber, undefined); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.endColumn, undefined); assert.strictEqual(editorService.lastInput!.resource.fragment, ''); }); @@ -61,17 +62,17 @@ suite('OpenerService', function () { const openerService = new OpenerService(editorService, NullCommandService); await openerService.open(URI.parse('file:///somepath#23')); - assert.strictEqual(editorService.lastInput!.options!.selection!.startLineNumber, 23); - assert.strictEqual(editorService.lastInput!.options!.selection!.startColumn, 1); - assert.strictEqual(editorService.lastInput!.options!.selection!.endLineNumber, undefined); - assert.strictEqual(editorService.lastInput!.options!.selection!.endColumn, undefined); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.startLineNumber, 23); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.startColumn, 1); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.endLineNumber, undefined); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.endColumn, undefined); assert.strictEqual(editorService.lastInput!.resource.fragment, ''); await openerService.open(URI.parse('file:///somepath#23,45')); - assert.strictEqual(editorService.lastInput!.options!.selection!.startLineNumber, 23); - assert.strictEqual(editorService.lastInput!.options!.selection!.startColumn, 45); - assert.strictEqual(editorService.lastInput!.options!.selection!.endLineNumber, undefined); - assert.strictEqual(editorService.lastInput!.options!.selection!.endColumn, undefined); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.startLineNumber, 23); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.startColumn, 45); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.endLineNumber, undefined); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.endColumn, undefined); assert.strictEqual(editorService.lastInput!.resource.fragment, ''); }); @@ -125,33 +126,6 @@ suite('OpenerService', function () { assert.strictEqual(httpsResult, false); }); - test('delegate to commandsService, command:someid', async function () { - const openerService = new OpenerService(editorService, commandService); - - const id = `aCommand${Math.random()}`; - CommandsRegistry.registerCommand(id, function () { }); - - await openerService.open(URI.parse('command:' + id).with({ query: '\"123\"' }), { allowCommands: true }); - assert.strictEqual(lastCommand!.id, id); - assert.strictEqual(lastCommand!.args.length, 1); - assert.strictEqual(lastCommand!.args[0], '123'); - - await openerService.open(URI.parse('command:' + id), { allowCommands: true }); - assert.strictEqual(lastCommand!.id, id); - assert.strictEqual(lastCommand!.args.length, 0); - - await openerService.open(URI.parse('command:' + id).with({ query: '123' }), { allowCommands: true }); - assert.strictEqual(lastCommand!.id, id); - assert.strictEqual(lastCommand!.args.length, 1); - assert.strictEqual(lastCommand!.args[0], 123); - - await openerService.open(URI.parse('command:' + id).with({ query: JSON.stringify([12, true]) }), { allowCommands: true }); - assert.strictEqual(lastCommand!.id, id); - assert.strictEqual(lastCommand!.args.length, 2); - assert.strictEqual(lastCommand!.args[0], 12); - assert.strictEqual(lastCommand!.args[1], true); - }); - test('links validated by validators go to openers', async function () { const openerService = new OpenerService(editorService, commandService); @@ -272,4 +246,25 @@ suite('OpenerService', function () { assert.ok(!matchesScheme(URI.parse('htt://microsoft.com'), 'http')); assert.ok(!matchesScheme(URI.parse('z://microsoft.com'), 'http')); }); + + test('resolveExternalUri', async function () { + const openerService = new OpenerService(editorService, NullCommandService); + + try { + await openerService.resolveExternalUri(URI.parse('file:///Users/user/folder')); + assert.fail('Should not reach here'); + } catch { + // OK + } + + const disposable = openerService.registerExternalUriResolver({ + async resolveExternalUri(uri) { + return { resolved: uri, dispose() { } }; + } + }); + + const result = await openerService.resolveExternalUri(URI.parse('file:///Users/user/folder')); + assert.deepStrictEqual(result.resolved.toString(), 'file:///Users/user/folder'); + disposable.dispose(); + }); }); diff --git a/src/vs/editor/test/common/core/stringBuilder.test.ts b/src/vs/editor/test/common/core/stringBuilder.test.ts index b8648d19f5..f6531df01d 100644 --- a/src/vs/editor/test/common/core/stringBuilder.test.ts +++ b/src/vs/editor/test/common/core/stringBuilder.test.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; diff --git a/src/vs/editor/test/common/diff/diffComputer.test.ts b/src/vs/editor/test/common/diff/diffComputer.test.ts index 307e1879eb..606dc645ee 100644 --- a/src/vs/editor/test/common/diff/diffComputer.test.ts +++ b/src/vs/editor/test/common/diff/diffComputer.test.ts @@ -916,4 +916,55 @@ suite('Editor Diff - DiffComputer', () => { ]; assertDiff(original, modified, expected, false, false, false); }); + + test('issue #121436: Diff chunk contains an unchanged line part 1', () => { + const original = [ + 'if (cond) {', + ' cmd', + '}', + ]; + const modified = [ + 'if (cond) {', + ' if (other_cond) {', + ' cmd', + ' }', + '}', + ]; + const expected = [ + createLineChange( + 1, 0, 2, 2 + ), + createLineChange( + 2, 0, 4, 4 + ) + ]; + assertDiff(original, modified, expected, false, false, true); + }); + + test('issue #121436: Diff chunk contains an unchanged line part 2', () => { + const original = [ + 'if (cond) {', + ' cmd', + '}', + ]; + const modified = [ + 'if (cond) {', + ' if (other_cond) {', + ' cmd', + ' }', + '}', + ]; + const expected = [ + createLineChange( + 1, 0, 2, 2 + ), + createLineChange( + 2, 2, 3, 3 + ), + createLineChange( + 2, 0, 4, 4 + ) + ]; + assertDiff(original, modified, expected, false, false, false); + }); }); diff --git a/src/vs/editor/test/common/model/benchmark/bootstrap.js b/src/vs/editor/test/common/model/benchmark/bootstrap.js index a1a121469b..51a50d1ebb 100644 --- a/src/vs/editor/test/common/model/benchmark/bootstrap.js +++ b/src/vs/editor/test/common/model/benchmark/bootstrap.js @@ -3,4 +3,4 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -require('../../../../../../bootstrap-amd').load('vs/editor/test/common/model/benchmark/entry'); \ No newline at end of file +require('../../../../../../bootstrap-amd').load('vs/editor/test/common/model/benchmark/entry'); diff --git a/src/vs/editor/test/common/model/benchmark/entry.ts b/src/vs/editor/test/common/model/benchmark/entry.ts index d87223350b..18fcc9d0ed 100644 --- a/src/vs/editor/test/common/model/benchmark/entry.ts +++ b/src/vs/editor/test/common/model/benchmark/entry.ts @@ -5,4 +5,4 @@ import 'vs/editor/test/common/model/benchmark/modelbuilder.benchmark'; import 'vs/editor/test/common/model/benchmark/operations.benchmark'; -import 'vs/editor/test/common/model/benchmark/searchNReplace.benchmark'; \ No newline at end of file +import 'vs/editor/test/common/model/benchmark/searchNReplace.benchmark'; diff --git a/src/vs/editor/test/common/model/editStack.test.ts b/src/vs/editor/test/common/model/editStack.test.ts index e554167552..3f1434894d 100644 --- a/src/vs/editor/test/common/model/editStack.test.ts +++ b/src/vs/editor/test/common/model/editStack.test.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index dda7fe6941..9c4632ef77 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -45,6 +45,7 @@ function modelHasNoDecorations(model: TextModel) { function addDecoration(model: TextModel, startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, className: string): string { return model.changeDecorations((changeAccessor) => { return changeAccessor.addDecoration(new Range(startLineNumber, startColumn, endLineNumber, endColumn), { + description: 'test', className: className }); })!; @@ -407,7 +408,7 @@ suite('Editor Model - Model Decorations', () => { }); test('removeAllDecorationsWithOwnerId works', () => { - thisModel.deltaDecorations([], [{ range: new Range(1, 2, 4, 1), options: { className: 'myType1' } }], 1); + thisModel.deltaDecorations([], [{ range: new Range(1, 2, 4, 1), options: { description: 'test', className: 'myType1' } }], 1); thisModel.removeAllDecorationsWithOwnerId(1); modelHasNoDecorations(thisModel); }); @@ -422,7 +423,7 @@ suite('Decorations and editing', () => { 'Third Line' ].join('\n')); - const id = model.deltaDecorations([], [{ range: decRange, options: { stickiness: stickiness } }])[0]; + const id = model.deltaDecorations([], [{ range: decRange, options: { description: 'test', stickiness: stickiness } }])[0]; model.applyEdits([{ range: editRange, text: editText, @@ -1123,6 +1124,7 @@ suite('deltaDecorations', () => { return { range: dec.range, options: { + description: 'test', className: dec.id } }; @@ -1276,6 +1278,7 @@ suite('deltaDecorations', () => { endColumn: 1 }, options: { + description: 'test', hoverMessage: { value: 'hello1' } } }]); @@ -1288,6 +1291,7 @@ suite('deltaDecorations', () => { endColumn: 1 }, options: { + description: 'test', hoverMessage: { value: 'hello2' } } }]); @@ -1312,9 +1316,11 @@ suite('deltaDecorations', () => { startColumn: 1, endLineNumber: 1, endColumn: 1 - }, { - stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges - } + }, + { + description: 'test', + stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges + } ); }); model.changeDecorations((changeAccessor) => { @@ -1349,16 +1355,16 @@ suite('deltaDecorations', () => { ].join('\n')); model.deltaDecorations([], [ - { range: new Range(1, 1, 1, 1), options: { className: '1' } }, - { range: new Range(1, 13, 1, 13), options: { className: '2' } }, - { range: new Range(2, 1, 2, 1), options: { className: '3' } }, - { range: new Range(2, 1, 2, 4), options: { className: '4' } }, - { range: new Range(2, 8, 2, 13), options: { className: '5' } }, - { range: new Range(3, 1, 4, 6), options: { className: '6' } }, - { range: new Range(1, 1, 3, 6), options: { className: 'x1' } }, - { range: new Range(2, 5, 2, 8), options: { className: 'x2' } }, - { range: new Range(1, 1, 2, 8), options: { className: 'x3' } }, - { range: new Range(2, 5, 3, 1), options: { className: 'x4' } }, + { range: new Range(1, 1, 1, 1), options: { description: 'test', className: '1' } }, + { range: new Range(1, 13, 1, 13), options: { description: 'test', className: '2' } }, + { range: new Range(2, 1, 2, 1), options: { description: 'test', className: '3' } }, + { range: new Range(2, 1, 2, 4), options: { description: 'test', className: '4' } }, + { range: new Range(2, 8, 2, 13), options: { description: 'test', className: '5' } }, + { range: new Range(3, 1, 4, 6), options: { description: 'test', className: '6' } }, + { range: new Range(1, 1, 3, 6), options: { description: 'test', className: 'x1' } }, + { range: new Range(2, 5, 2, 8), options: { description: 'test', className: 'x2' } }, + { range: new Range(1, 1, 2, 8), options: { description: 'test', className: 'x3' } }, + { range: new Range(2, 5, 3, 1), options: { description: 'test', className: 'x4' } }, ]); let inRange = model.getDecorationsInRange(new Range(2, 6, 2, 6)); @@ -1376,7 +1382,7 @@ suite('deltaDecorations', () => { 'My First Line' ].join('\n')); - const id = model.deltaDecorations([], [{ range: new Range(1, 2, 1, 14), options: { stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, collapseOnReplaceEdit: true } }])[0]; + const id = model.deltaDecorations([], [{ range: new Range(1, 2, 1, 14), options: { description: 'test', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, collapseOnReplaceEdit: true } }])[0]; model.applyEdits([{ range: new Range(1, 1, 1, 14), text: 'Some new text that is longer than the previous one', diff --git a/src/vs/editor/test/common/modes/linkComputer.test.ts b/src/vs/editor/test/common/modes/linkComputer.test.ts index 13974ff90a..a73ba587b4 100644 --- a/src/vs/editor/test/common/modes/linkComputer.test.ts +++ b/src/vs/editor/test/common/modes/linkComputer.test.ts @@ -230,4 +230,25 @@ suite('Editor Modes - Link Computer', () => { ' http://tree-mark.chips.jp/レーズン&ベリーミックス ' ); }); + + test('issue #121438: Link detection stops at【...】', () => { + assertLink( + 'aa https://zh.wikipedia.org/wiki/【我推的孩子】 aa', + ' https://zh.wikipedia.org/wiki/【我推的孩子】 ' + ); + }); + + test('issue #121438: Link detection stops at《...》', () => { + assertLink( + 'aa https://zh.wikipedia.org/wiki/《新青年》编辑部旧址 aa', + ' https://zh.wikipedia.org/wiki/《新青年》编辑部旧址 ' + ); + }); + + test('issue #121438: Link detection stops at “...”', () => { + assertLink( + 'aa https://zh.wikipedia.org/wiki/“常凯申”误译事件 aa', + ' https://zh.wikipedia.org/wiki/“常凯申”误译事件 ' + ); + }); }); 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 05d67e27b1..505c62eee6 100644 --- a/src/vs/editor/test/common/modes/supports/onEnter.test.ts +++ b/src/vs/editor/test/common/modes/supports/onEnter.test.ts @@ -47,6 +47,40 @@ suite('OnEnter', () => { testIndentAction('begin', '', IndentAction.Indent); }); + + test('Issue #121125: onEnterRules with global modifier', () => { + const support = new OnEnterSupport({ + onEnterRules: [ + { + action: { + appendText: '/// ', + indentAction: IndentAction.Outdent + }, + beforeText: /^\s*\/{3}.*$/gm + } + ] + }); + + 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.strictEqual(actual, null, 'isNull:' + beforeText); + } else { + assert.strictEqual(actual !== null, true, 'isNotNull:' + beforeText); + assert.strictEqual(actual!.indentAction, expectedIndentAction, 'indentAction:' + beforeText); + if (expectedAppendText !== null) { + assert.strictEqual(actual!.appendText, expectedAppendText, 'appendText:' + beforeText); + } + if (removeText !== 0) { + assert.strictEqual(actual!.removeText, removeText, 'removeText:' + beforeText); + } + } + }; + + testIndentAction('/// line', '/// line', '', IndentAction.Outdent, '/// '); + testIndentAction('/// line', '/// line', '', IndentAction.Outdent, '/// '); + }); + test('uses regExpRules', () => { let support = new OnEnterSupport({ onEnterRules: javascriptOnEnterRules diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index 2f2b36c157..8de3f28523 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -9,7 +9,7 @@ import * as strings from 'vs/base/common/strings'; import { IViewLineTokens } from 'vs/editor/common/core/lineTokens'; import { MetadataConsts } from 'vs/editor/common/modes'; import { LineDecoration } from 'vs/editor/common/viewLayout/lineDecorations'; -import { CharacterMapping, RenderLineInput, renderViewLine2 as renderViewLine, LineRange } from 'vs/editor/common/viewLayout/viewLineRenderer'; +import { CharacterMapping, RenderLineInput, renderViewLine2 as renderViewLine, LineRange, DomPosition } from 'vs/editor/common/viewLayout/viewLineRenderer'; import { InlineDecorationType } from 'vs/editor/common/viewModel/viewModel'; import { ViewLineToken, ViewLineTokens } from 'vs/editor/test/common/core/viewLineToken'; @@ -25,8 +25,8 @@ function createPart(endIndex: number, foreground: number): ViewLineToken { suite('viewLineRenderer.renderLine', () => { - function assertCharacterReplacement(lineContent: string, tabSize: number, expected: string, expectedCharOffsetInPart: number[][], expectedPartLengts: number[]): void { - let _actual = renderViewLine(new RenderLineInput( + function assertCharacterReplacement(lineContent: string, tabSize: number, expected: string, expectedCharOffsetInPart: number[]): void { + const _actual = renderViewLine(new RenderLineInput( false, true, lineContent, @@ -49,36 +49,37 @@ suite('viewLineRenderer.renderLine', () => { )); assert.strictEqual(_actual.html, '' + expected + ''); - assertCharacterMapping(_actual.characterMapping, expectedCharOffsetInPart, expectedPartLengts); + const info = expectedCharOffsetInPart.map((absoluteOffset) => [absoluteOffset, [0, absoluteOffset]]); + assertCharacterMapping3(_actual.characterMapping, info); } test('replaces spaces', () => { - assertCharacterReplacement(' ', 4, '\u00a0', [[0, 1]], [1]); - assertCharacterReplacement(' ', 4, '\u00a0\u00a0', [[0, 1, 2]], [2]); - assertCharacterReplacement('a b', 4, 'a\u00a0\u00a0b', [[0, 1, 2, 3, 4]], [4]); + assertCharacterReplacement(' ', 4, '\u00a0', [0, 1]); + assertCharacterReplacement(' ', 4, '\u00a0\u00a0', [0, 1, 2]); + assertCharacterReplacement('a b', 4, 'a\u00a0\u00a0b', [0, 1, 2, 3, 4]); }); test('escapes HTML markup', () => { - assertCharacterReplacement('ab', 4, 'a>b', [[0, 1, 2, 3]], [3]); - assertCharacterReplacement('a&b', 4, 'a&b', [[0, 1, 2, 3]], [3]); + assertCharacterReplacement('ab', 4, 'a>b', [0, 1, 2, 3]); + assertCharacterReplacement('a&b', 4, 'a&b', [0, 1, 2, 3]); }); test('replaces some bad characters', () => { - assertCharacterReplacement('a\0b', 4, 'a�b', [[0, 1, 2, 3]], [3]); - assertCharacterReplacement('a' + String.fromCharCode(CharCode.UTF8_BOM) + 'b', 4, 'a\ufffdb', [[0, 1, 2, 3]], [3]); - assertCharacterReplacement('a\u2028b', 4, 'a\ufffdb', [[0, 1, 2, 3]], [3]); + assertCharacterReplacement('a\0b', 4, 'a�b', [0, 1, 2, 3]); + assertCharacterReplacement('a' + String.fromCharCode(CharCode.UTF8_BOM) + 'b', 4, 'a\ufffdb', [0, 1, 2, 3]); + assertCharacterReplacement('a\u2028b', 4, 'a\ufffdb', [0, 1, 2, 3]); }); test('handles tabs', () => { - assertCharacterReplacement('\t', 4, '\u00a0\u00a0\u00a0\u00a0', [[0, 4]], [4]); - assertCharacterReplacement('x\t', 4, 'x\u00a0\u00a0\u00a0', [[0, 1, 4]], [4]); - assertCharacterReplacement('xx\t', 4, 'xx\u00a0\u00a0', [[0, 1, 2, 4]], [4]); - assertCharacterReplacement('xxx\t', 4, 'xxx\u00a0', [[0, 1, 2, 3, 4]], [4]); - assertCharacterReplacement('xxxx\t', 4, 'xxxx\u00a0\u00a0\u00a0\u00a0', [[0, 1, 2, 3, 4, 8]], [8]); + assertCharacterReplacement('\t', 4, '\u00a0\u00a0\u00a0\u00a0', [0, 4]); + assertCharacterReplacement('x\t', 4, 'x\u00a0\u00a0\u00a0', [0, 1, 4]); + assertCharacterReplacement('xx\t', 4, 'xx\u00a0\u00a0', [0, 1, 2, 4]); + assertCharacterReplacement('xxx\t', 4, 'xxx\u00a0', [0, 1, 2, 3, 4]); + assertCharacterReplacement('xxxx\t', 4, 'xxxx\u00a0\u00a0\u00a0\u00a0', [0, 1, 2, 3, 4, 8]); }); - function assertParts(lineContent: string, tabSize: number, parts: ViewLineToken[], expected: string, expectedCharOffsetInPart: number[][], expectedPartLengts: number[]): void { + function assertParts(lineContent: string, tabSize: number, parts: ViewLineToken[], expected: string, info: CharacterMappingInfo[]): void { let _actual = renderViewLine(new RenderLineInput( false, true, @@ -102,23 +103,23 @@ suite('viewLineRenderer.renderLine', () => { )); assert.strictEqual(_actual.html, '' + expected + ''); - assertCharacterMapping(_actual.characterMapping, expectedCharOffsetInPart, expectedPartLengts); + assertCharacterMapping3(_actual.characterMapping, info); } test('empty line', () => { - assertParts('', 4, [], '', [], []); + assertParts('', 4, [], '', []); }); test('uses part type', () => { - assertParts('x', 4, [createPart(1, 10)], 'x', [[0, 1]], [1]); - assertParts('x', 4, [createPart(1, 20)], 'x', [[0, 1]], [1]); - assertParts('x', 4, [createPart(1, 30)], 'x', [[0, 1]], [1]); + assertParts('x', 4, [createPart(1, 10)], 'x', [[0, [0, 0]], [1, [0, 1]]]); + assertParts('x', 4, [createPart(1, 20)], 'x', [[0, [0, 0]], [1, [0, 1]]]); + assertParts('x', 4, [createPart(1, 30)], 'x', [[0, [0, 0]], [1, [0, 1]]]); }); test('two parts', () => { - assertParts('xy', 4, [createPart(1, 1), createPart(2, 2)], 'xy', [[0], [0, 1]], [1, 1]); - assertParts('xyz', 4, [createPart(1, 1), createPart(3, 2)], 'xyz', [[0], [0, 1, 2]], [1, 2]); - assertParts('xyz', 4, [createPart(2, 1), createPart(3, 2)], 'xyz', [[0, 1], [0, 1]], [2, 1]); + assertParts('xy', 4, [createPart(1, 1), createPart(2, 2)], 'xy', [[0, [0, 0]], [1, [1, 0]], [2, [1, 1]]]); + assertParts('xyz', 4, [createPart(1, 1), createPart(3, 2)], 'xyz', [[0, [0, 0]], [1, [1, 0]], [2, [1, 1]], [3, [1, 2]]]); + assertParts('xyz', 4, [createPart(2, 1), createPart(3, 2)], 'xyz', [[0, [0, 0]], [1, [0, 1]], [2, [1, 0]], [3, [1, 1]]]); }); test('overflow', () => { @@ -168,16 +169,17 @@ suite('viewLineRenderer.renderLine', () => { ].join(''); assert.strictEqual(_actual.html, '' + expectedOutput + ''); - assertCharacterMapping(_actual.characterMapping, + assertCharacterMapping3( + _actual.characterMapping, [ - [0], - [0], - [0], - [0], - [0], - [0, 1], - ], - [1, 1, 1, 1, 1, 1] + [0, [0, 0]], + [1, [1, 0]], + [2, [2, 0]], + [3, [3, 0]], + [4, [4, 0]], + [5, [5, 0]], + [6, [5, 1]], + ] ); }); @@ -213,24 +215,25 @@ suite('viewLineRenderer.renderLine', () => { '\u00b7\u00b7', '\u00b7\u00b7\u00b7' ].join(''); - let expectedOffsetsArr = [ - [0], - [0, 1, 2, 3], - [0, 1, 2, 3, 4, 5], - [0], - [0, 1, 2, 3, 4], - [0], - [0, 1, 2, 3], - [0], - [0], - [0], - [0, 1, 2], - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], - [0, 1], - [0, 1, 2, 3], + + const info: CharacterMappingInfo[] = [ + [0, [0, 0]], + [4, [1, 0]], [5, [1, 1]], [6, [1, 2]], [7, [1, 3]], + [8, [2, 0]], [9, [2, 1]], [10, [2, 2]], [11, [2, 3]], [12, [2, 4]], [13, [2, 5]], + [14, [3, 0]], + [15, [4, 0]], [16, [4, 1]], [17, [4, 2]], [18, [4, 3]], [19, [4, 4]], + [20, [5, 0]], + [21, [6, 0]], [22, [6, 1]], [23, [6, 2]], [24, [6, 3]], + [25, [7, 0]], + [26, [8, 0]], + [27, [9, 0]], + [28, [10, 0]], [29, [10, 1]], [30, [10, 2]], + [31, [11, 0]], [32, [11, 1]], [33, [11, 2]], [34, [11, 3]], [35, [11, 4]], [36, [11, 5]], [37, [11, 6]], [38, [11, 7]], [39, [11, 8]], [40, [11, 9]], [41, [11, 10]], [42, [11, 11]], [43, [11, 12]], [44, [11, 13]], [45, [11, 14]], + [46, [12, 0]], [47, [12, 1]], + [48, [13, 0]], [49, [13, 1]], [50, [13, 2]], [51, [13, 3]], ]; - let _actual = renderViewLine(new RenderLineInput( + const _actual = renderViewLine(new RenderLineInput( false, true, lineText, @@ -253,7 +256,7 @@ suite('viewLineRenderer.renderLine', () => { )); assert.strictEqual(_actual.html, '' + expectedOutput + ''); - assertCharacterMapping(_actual.characterMapping, expectedOffsetsArr, [4, 4, 6, 1, 5, 1, 4, 1, 1, 1, 3, 15, 2, 3]); + assertCharacterMapping3(_actual.characterMapping, info); }); test('issue #2255: Weird line rendering part 1', () => { @@ -283,20 +286,21 @@ suite('viewLineRenderer.renderLine', () => { ')', ',', ].join(''); - let expectedOffsetsArr = [ - [0, 4, 8], // 3 chars - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], // 12 chars - [0, 4, 8, 12, 16, 20], // 6 chars - [0], // 1 char - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], // 21 chars - [0, 1], // 2 chars - [0], // 1 char - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], // 20 chars - [0], // 1 char - [0, 1] // 2 chars + + const info: CharacterMappingInfo[] = [ + [0, [0, 0]], [4, [0, 4]], [8, [0, 8]], + [12, [1, 0]], [13, [1, 1]], [14, [1, 2]], [15, [1, 3]], [16, [1, 4]], [17, [1, 5]], [18, [1, 6]], [19, [1, 7]], [20, [1, 8]], [21, [1, 9]], [22, [1, 10]], [23, [1, 11]], + [24, [2, 0]], [28, [2, 4]], [32, [2, 8]], [36, [2, 12]], [40, [2, 16]], [44, [2, 20]], + [48, [3, 0]], + [49, [4, 0]], [50, [4, 1]], [51, [4, 2]], [52, [4, 3]], [53, [4, 4]], [54, [4, 5]], [55, [4, 6]], [56, [4, 7]], [57, [4, 8]], [58, [4, 9]], [59, [4, 10]], [60, [4, 11]], [61, [4, 12]], [62, [4, 13]], [63, [4, 14]], [64, [4, 15]], [65, [4, 16]], [66, [4, 17]], [67, [4, 18]], [68, [4, 19]], [69, [4, 20]], + [70, [5, 0]], [71, [5, 1]], + [72, [6, 0]], + [73, [7, 0]], [74, [7, 1]], [75, [7, 2]], [76, [7, 3]], [77, [7, 4]], [78, [7, 5]], [79, [7, 6]], [80, [7, 7]], [81, [7, 8]], [82, [7, 9]], [83, [7, 10]], [84, [7, 11]], [85, [7, 12]], [86, [7, 13]], [87, [7, 14]], [88, [7, 15]], [89, [7, 16]], [90, [7, 17]], [91, [7, 18]], [92, [7, 19]], + [93, [8, 0]], + [94, [9, 0]], [95, [9, 1]], ]; - let _actual = renderViewLine(new RenderLineInput( + const _actual = renderViewLine(new RenderLineInput( false, true, lineText, @@ -319,7 +323,7 @@ suite('viewLineRenderer.renderLine', () => { )); assert.strictEqual(_actual.html, '' + expectedOutput + ''); - assertCharacterMapping(_actual.characterMapping, expectedOffsetsArr, [12, 12, 24, 1, 21, 2, 1, 20, 1, 1]); + assertCharacterMapping3(_actual.characterMapping, info); }); test('issue #2255: Weird line rendering part 2', () => { @@ -349,20 +353,21 @@ suite('viewLineRenderer.renderLine', () => { ')', ',', ].join(''); - let expectedOffsetsArr = [ - [0, 1, 4, 8], // 4 chars - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], // 12 chars - [0, 4, 8, 12, 16, 20], // 6 chars - [0], // 1 char - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], // 21 chars - [0, 1], // 2 chars - [0], // 1 char - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], // 20 chars - [0], // 1 char - [0, 1] // 2 chars + + const info: CharacterMappingInfo[] = [ + [0, [0, 0]], [1, [0, 1]], [4, [0, 4]], [8, [0, 8]], + [12, [1, 0]], [13, [1, 1]], [14, [1, 2]], [15, [1, 3]], [16, [1, 4]], [17, [1, 5]], [18, [1, 6]], [19, [1, 7]], [20, [1, 8]], [21, [1, 9]], [22, [1, 10]], [23, [1, 11]], + [24, [2, 0]], [28, [2, 4]], [32, [2, 8]], [36, [2, 12]], [40, [2, 16]], [44, [2, 20]], + [48, [3, 0]], + [49, [4, 0]], [50, [4, 1]], [51, [4, 2]], [52, [4, 3]], [53, [4, 4]], [54, [4, 5]], [55, [4, 6]], [56, [4, 7]], [57, [4, 8]], [58, [4, 9]], [59, [4, 10]], [60, [4, 11]], [61, [4, 12]], [62, [4, 13]], [63, [4, 14]], [64, [4, 15]], [65, [4, 16]], [66, [4, 17]], [67, [4, 18]], [68, [4, 19]], [69, [4, 20]], + [70, [5, 0]], [71, [5, 1]], + [72, [6, 0]], + [73, [7, 0]], [74, [7, 1]], [75, [7, 2]], [76, [7, 3]], [77, [7, 4]], [78, [7, 5]], [79, [7, 6]], [80, [7, 7]], [81, [7, 8]], [82, [7, 9]], [83, [7, 10]], [84, [7, 11]], [85, [7, 12]], [86, [7, 13]], [87, [7, 14]], [88, [7, 15]], [89, [7, 16]], [90, [7, 17]], [91, [7, 18]], [92, [7, 19]], + [93, [8, 0]], + [94, [9, 0]], [95, [9, 1]], ]; - let _actual = renderViewLine(new RenderLineInput( + const _actual = renderViewLine(new RenderLineInput( false, true, lineText, @@ -385,7 +390,7 @@ suite('viewLineRenderer.renderLine', () => { )); assert.strictEqual(_actual.html, '' + expectedOutput + ''); - assertCharacterMapping(_actual.characterMapping, expectedOffsetsArr, [12, 12, 24, 1, 21, 2, 1, 20, 1, 1]); + assertCharacterMapping3(_actual.characterMapping, info); }); test('issue #91178: after decoration type shown before cursor', () => { @@ -401,23 +406,23 @@ suite('viewLineRenderer.renderLine', () => { ].join(''); const expectedCharacterMapping = new CharacterMapping(17, 4); - expectedCharacterMapping.setPartData(0, 0, 0, 0); - expectedCharacterMapping.setPartData(1, 0, 1, 0); - expectedCharacterMapping.setPartData(2, 0, 2, 0); - expectedCharacterMapping.setPartData(3, 0, 3, 0); - expectedCharacterMapping.setPartData(4, 0, 4, 0); - expectedCharacterMapping.setPartData(5, 0, 5, 0); - expectedCharacterMapping.setPartData(6, 0, 6, 0); - expectedCharacterMapping.setPartData(7, 0, 7, 0); - expectedCharacterMapping.setPartData(8, 0, 8, 0); - expectedCharacterMapping.setPartData(9, 0, 9, 0); - expectedCharacterMapping.setPartData(10, 0, 10, 0); - expectedCharacterMapping.setPartData(11, 0, 11, 0); - expectedCharacterMapping.setPartData(12, 2, 0, 12); - expectedCharacterMapping.setPartData(13, 3, 1, 12); - expectedCharacterMapping.setPartData(14, 3, 2, 12); - expectedCharacterMapping.setPartData(15, 3, 3, 12); - expectedCharacterMapping.setPartData(16, 3, 4, 12); + expectedCharacterMapping.setColumnInfo(1, 0, 0, 0); + expectedCharacterMapping.setColumnInfo(2, 0, 1, 0); + expectedCharacterMapping.setColumnInfo(3, 0, 2, 0); + expectedCharacterMapping.setColumnInfo(4, 0, 3, 0); + expectedCharacterMapping.setColumnInfo(5, 0, 4, 0); + expectedCharacterMapping.setColumnInfo(6, 0, 5, 0); + expectedCharacterMapping.setColumnInfo(7, 0, 6, 0); + expectedCharacterMapping.setColumnInfo(8, 0, 7, 0); + expectedCharacterMapping.setColumnInfo(9, 0, 8, 0); + expectedCharacterMapping.setColumnInfo(10, 0, 9, 0); + expectedCharacterMapping.setColumnInfo(11, 0, 10, 0); + expectedCharacterMapping.setColumnInfo(12, 0, 11, 0); + expectedCharacterMapping.setColumnInfo(13, 2, 0, 12); + expectedCharacterMapping.setColumnInfo(14, 3, 1, 12); + expectedCharacterMapping.setColumnInfo(15, 3, 2, 12); + expectedCharacterMapping.setColumnInfo(16, 3, 3, 12); + expectedCharacterMapping.setColumnInfo(17, 3, 4, 12); const actual = renderViewLine(new RenderLineInput( true, @@ -792,14 +797,12 @@ suite('viewLineRenderer.renderLine', () => { function decodeCharacterMapping(source: CharacterMapping) { const mapping: ICharMappingData[] = []; for (let charOffset = 0; charOffset < source.length; charOffset++) { - const partData = source.charOffsetToPartData(charOffset); - const partIndex = CharacterMapping.getPartIndex(partData); - const charIndex = CharacterMapping.getCharIndex(partData); - mapping.push({ charOffset, partIndex, charIndex }); + const domPosition = source.getDomPosition(charOffset + 1); + mapping.push({ charOffset, partIndex: domPosition.partIndex, charIndex: domPosition.charIndex }); } const absoluteOffsets: number[] = []; - for (const absoluteOffset of source.getAbsoluteOffsets()) { - absoluteOffsets.push(absoluteOffset); + for (let i = 0; i < source.length; i++) { + absoluteOffsets[i] = source.getAbsoluteOffset(i + 1); } return { mapping, absoluteOffsets }; } @@ -809,64 +812,37 @@ suite('viewLineRenderer.renderLine', () => { const _expected = decodeCharacterMapping(expected); assert.deepStrictEqual(_actual, _expected); } - - function assertCharacterMapping(actual: CharacterMapping, expectedCharPartOffsets: number[][], expectedPartLengths: number[]): void { - - assertCharPartOffsets(actual, expectedCharPartOffsets); - - let expectedCharAbsoluteOffset: number[] = [], currentPartAbsoluteOffset = 0; - for (let partIndex = 0; partIndex < expectedCharPartOffsets.length; partIndex++) { - const part = expectedCharPartOffsets[partIndex]; - - for (const charIndex of part) { - expectedCharAbsoluteOffset.push(currentPartAbsoluteOffset + charIndex); - } - - currentPartAbsoluteOffset += expectedPartLengths[partIndex]; - } - - let actualCharOffset: number[] = []; - let tmp = actual.getAbsoluteOffsets(); - for (let i = 0; i < tmp.length; i++) { - actualCharOffset[i] = tmp[i]; - } - assert.deepStrictEqual(actualCharOffset, expectedCharAbsoluteOffset); - } - - function assertCharPartOffsets(actual: CharacterMapping, expected: number[][]): void { - - let charOffset = 0; - for (let partIndex = 0; partIndex < expected.length; partIndex++) { - let part = expected[partIndex]; - for (const charIndex of part) { - // here - let _actualPartData = actual.charOffsetToPartData(charOffset); - let actualPartIndex = CharacterMapping.getPartIndex(_actualPartData); - let actualCharIndex = CharacterMapping.getCharIndex(_actualPartData); - - assert.deepStrictEqual( - { partIndex: actualPartIndex, charIndex: actualCharIndex }, - { partIndex: partIndex, charIndex: charIndex }, - `character mapping for offset ${charOffset}` - ); - - // here - let actualOffset = actual.partDataToCharOffset(partIndex, part[part.length - 1] + 1, charIndex); - - assert.strictEqual( - actualOffset, - charOffset, - `character mapping for part ${partIndex}, ${charIndex}` - ); - - charOffset++; - } - } - - assert.strictEqual(actual.length, charOffset); - } }); +type CharacterMappingInfo = [number, [number, number]]; + +function assertCharacterMapping3(actual: CharacterMapping, expectedInfo: CharacterMappingInfo[]): void { + for (let i = 0; i < expectedInfo.length; i++) { + const [absoluteOffset, [partIndex, charIndex]] = expectedInfo[i]; + + const actualDomPosition = actual.getDomPosition(i + 1); + assert.deepStrictEqual(actualDomPosition, new DomPosition(partIndex, charIndex), `getDomPosition(${i + 1})`); + + let partLength = charIndex + 1; + for (let j = i + 1; j < expectedInfo.length; j++) { + const [, [nextPartIndex, nextCharIndex]] = expectedInfo[j]; + if (nextPartIndex === partIndex) { + partLength = nextCharIndex + 1; + } else { + break; + } + } + + const actualColumn = actual.getColumn(new DomPosition(partIndex, charIndex), partLength); + assert.strictEqual(actualColumn, i + 1, `actual.getColumn(${partIndex}, ${charIndex})`); + + const actualAbsoluteOffset = actual.getAbsoluteOffset(i + 1); + assert.strictEqual(actualAbsoluteOffset, absoluteOffset, `actual.getAbsoluteOffset(${i + 1})`); + } + + assert.strictEqual(actual.length, expectedInfo.length, `length mismatch`); +} + suite('viewLineRenderer.renderLine 2', () => { function testCreateLineParts(fontIsMonospace: boolean, lineContent: string, tokens: ViewLineToken[], fauxIndentLength: number, renderWhitespace: 'none' | 'boundary' | 'selection' | 'trailing' | 'all', selections: LineRange[] | null, expected: string): void { @@ -1739,7 +1715,7 @@ suite('viewLineRenderer.renderLine 2', () => { let expected = [ '', '\u00a0\u00a0\u00a0\u00a0}', - '', + '', '' ].join(''); @@ -2138,6 +2114,53 @@ suite('viewLineRenderer.renderLine 2', () => { assert.deepStrictEqual(actual.html, expected); }); + test('issue #124038: Multiple end-of-line text decorations get merged', () => { + const actual = renderViewLine(new RenderLineInput( + true, + false, + ' if', + false, + true, + false, + 0, + createViewLineTokens([createPart(4, 1), createPart(6, 2)]), + [ + new LineDecoration(7, 7, 'ced-1-TextEditorDecorationType2-17c14d98-3 ced-1-TextEditorDecorationType2-3', InlineDecorationType.Before), + new LineDecoration(7, 7, 'ced-1-TextEditorDecorationType2-17c14d98-4 ced-1-TextEditorDecorationType2-4', InlineDecorationType.After), + new LineDecoration(7, 7, 'ced-ghost-text-1-4', InlineDecorationType.After), + ], + 4, + 0, + 10, + 10, + 10, + 10000, + 'all', + false, + false, + null + )); + + const expected = [ + '', + '····if', + '' + ].join(''); + + assert.deepStrictEqual(actual.html, expected); + assertCharacterMapping3(actual.characterMapping, + [ + [0, [0, 0]], + [1, [0, 1]], + [2, [0, 2]], + [3, [0, 3]], + [4, [1, 0]], + [5, [1, 1]], + [6, [3, 0]], + ] + ); + }); + function createTestGetColumnOfLinePartOffset(lineContent: string, tabSize: number, parts: ViewLineToken[], expectedPartLengths: number[]): (partIndex: number, partLength: number, offset: number, expected: number) => void { let renderLineOutput = renderViewLine(new RenderLineInput( @@ -2163,9 +2186,8 @@ 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.strictEqual(actual, expected, 'getColumnOfLinePartOffset for ' + partIndex + ' @ ' + offset); + const actualColumn = renderLineOutput.characterMapping.getColumn(new DomPosition(partIndex, offset), partLength); + assert.strictEqual(actualColumn, expected, 'getColumn for ' + partIndex + ', ' + offset); }; } diff --git a/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts b/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts index af4043ea15..a2e8c6754a 100644 --- a/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts +++ b/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts @@ -28,6 +28,7 @@ suite('ViewModelDecorations', () => { model.changeDecorations((accessor) => { let createOpts = (id: string) => { return { + description: 'test', className: id, inlineClassName: 'i-' + id, beforeContentClassName: 'b-' + id, @@ -165,6 +166,7 @@ suite('ViewModelDecorations', () => { accessor.addDecoration( new Range(1, 50, 1, 51), { + description: 'test', beforeContentClassName: 'dec1' } ); @@ -199,6 +201,7 @@ suite('ViewModelDecorations', () => { accessor.addDecoration( new Range(1, 1, 1, 1), { + description: 'test', beforeContentClassName: 'before1', afterContentClassName: 'after1' } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index e73da0f6b8..4091617a9a 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1983,6 +1983,11 @@ declare namespace monaco.editor { * @event */ onDidChangeLanguageConfiguration(listener: (e: IModelLanguageConfigurationChangedEvent) => void): IDisposable; + /** + * An event emitted when the model has been attached to the first editor or detached from the last editor. + * @event + */ + onDidChangeAttached(listener: () => void): IDisposable; /** * An event emitted right before disposing the model. * @event @@ -1993,6 +1998,10 @@ declare namespace monaco.editor { * and make all necessary clean-up to release this object to the GC. */ dispose(): void; + /** + * Returns if this model is attached to an editor or not. + */ + isAttachedToEditor(): boolean; } /** @@ -2964,8 +2973,9 @@ declare namespace monaco.editor { * Suggest options. */ suggest?: ISuggestOptions; + inlineSuggest?: IInlineSuggestOptions; /** - * Smart select opptions; + * Smart select options. */ smartSelect?: ISmartSelectOptions; /** @@ -3219,7 +3229,11 @@ declare namespace monaco.editor { /** * Control the behavior and rendering of the inline hints. */ - inlineHints?: IEditorInlineHintsOptions; + inlayHints?: IEditorInlayHintsOptions; + /** + * Control if the editor should use shadow DOM. + */ + useShadowDOM?: boolean; } /** @@ -3584,9 +3598,9 @@ declare namespace monaco.editor { export type EditorLightbulbOptions = Readonly>; /** - * Configuration options for editor inlineHints + * Configuration options for editor inlayHints */ - export interface IEditorInlineHintsOptions { + export interface IEditorInlayHintsOptions { /** * Enable the inline hints. * Defaults to true. @@ -3604,7 +3618,7 @@ declare namespace monaco.editor { fontFamily?: string; } - export type EditorInlineHintsOptions = Readonly>; + export type EditorInlayHintsOptions = Readonly>; /** * Configuration options for editor minimap @@ -3803,6 +3817,15 @@ declare namespace monaco.editor { readonly scrollByPage: boolean; } + export interface IInlineSuggestOptions { + /** + * Enable or disable the rendering of automatic inline completions. + */ + enabled?: boolean; + } + + export type InternalInlineSuggestOptions = Readonly>; + /** * Configuration options for editor suggest widget */ @@ -3835,6 +3858,10 @@ declare namespace monaco.editor { * Enable or disable the suggest status bar. */ showStatusBar?: boolean; + /** + * Enable or disable the rendering of the suggestion preview. + */ + preview?: boolean; /** * Show details inline with the label. Defaults to true. */ @@ -3851,6 +3878,10 @@ declare namespace monaco.editor { * Show constructor-suggestions. */ showConstructors?: boolean; + /** + * Show deprecated-suggestions. + */ + showDeprecated?: boolean; /** * Show field-suggestions. */ @@ -4039,82 +4070,84 @@ declare namespace monaco.editor { highlightActiveIndentGuide = 49, hover = 50, inDiffEditor = 51, - letterSpacing = 52, - lightbulb = 53, - lineDecorationsWidth = 54, - lineHeight = 55, - lineNumbers = 56, - lineNumbersMinChars = 57, - linkedEditing = 58, - links = 59, - matchBrackets = 60, - minimap = 61, - mouseStyle = 62, - mouseWheelScrollSensitivity = 63, - mouseWheelZoom = 64, - multiCursorMergeOverlapping = 65, - multiCursorModifier = 66, - multiCursorPaste = 67, - occurrencesHighlight = 68, - overviewRulerBorder = 69, - overviewRulerLanes = 70, - padding = 71, - parameterHints = 72, - peekWidgetDefaultFocus = 73, - definitionLinkOpensInPeek = 74, - quickSuggestions = 75, - quickSuggestionsDelay = 76, - readOnly = 77, - renameOnType = 78, - renderControlCharacters = 79, - renderIndentGuides = 80, - renderFinalNewline = 81, - renderLineHighlight = 82, - renderLineHighlightOnlyWhenFocus = 83, - renderValidationDecorations = 84, - renderWhitespace = 85, - revealHorizontalRightPadding = 86, - roundedSelection = 87, - rulers = 88, - scrollbar = 89, - scrollBeyondLastColumn = 90, - scrollBeyondLastLine = 91, - scrollPredominantAxis = 92, - selectionClipboard = 93, - selectionHighlight = 94, - selectOnLineNumbers = 95, - showFoldingControls = 96, - showUnused = 97, - snippetSuggestions = 98, - smartSelect = 99, - smoothScrolling = 100, - stickyTabStops = 101, - stopRenderingLineAfter = 102, - suggest = 103, - suggestFontSize = 104, - suggestLineHeight = 105, - suggestOnTriggerCharacters = 106, - suggestSelection = 107, - tabCompletion = 108, - tabIndex = 109, - unusualLineTerminators = 110, - useTabStops = 111, - wordSeparators = 112, - wordWrap = 113, - wordWrapBreakAfterCharacters = 114, - wordWrapBreakBeforeCharacters = 115, - wordWrapColumn = 116, - wordWrapOverride1 = 117, - wordWrapOverride2 = 118, - wrappingIndent = 119, - wrappingStrategy = 120, - showDeprecated = 121, - inlineHints = 122, - editorClassName = 123, - pixelRatio = 124, - tabFocusMode = 125, - layoutInfo = 126, - wrappingInfo = 127 + inlineSuggest = 52, + letterSpacing = 53, + lightbulb = 54, + lineDecorationsWidth = 55, + lineHeight = 56, + lineNumbers = 57, + lineNumbersMinChars = 58, + linkedEditing = 59, + links = 60, + matchBrackets = 61, + minimap = 62, + mouseStyle = 63, + mouseWheelScrollSensitivity = 64, + mouseWheelZoom = 65, + multiCursorMergeOverlapping = 66, + multiCursorModifier = 67, + multiCursorPaste = 68, + occurrencesHighlight = 69, + overviewRulerBorder = 70, + overviewRulerLanes = 71, + padding = 72, + parameterHints = 73, + peekWidgetDefaultFocus = 74, + definitionLinkOpensInPeek = 75, + quickSuggestions = 76, + quickSuggestionsDelay = 77, + readOnly = 78, + renameOnType = 79, + renderControlCharacters = 80, + renderIndentGuides = 81, + renderFinalNewline = 82, + renderLineHighlight = 83, + renderLineHighlightOnlyWhenFocus = 84, + renderValidationDecorations = 85, + renderWhitespace = 86, + revealHorizontalRightPadding = 87, + roundedSelection = 88, + rulers = 89, + scrollbar = 90, + scrollBeyondLastColumn = 91, + scrollBeyondLastLine = 92, + scrollPredominantAxis = 93, + selectionClipboard = 94, + selectionHighlight = 95, + selectOnLineNumbers = 96, + showFoldingControls = 97, + showUnused = 98, + snippetSuggestions = 99, + smartSelect = 100, + smoothScrolling = 101, + stickyTabStops = 102, + stopRenderingLineAfter = 103, + suggest = 104, + suggestFontSize = 105, + suggestLineHeight = 106, + suggestOnTriggerCharacters = 107, + suggestSelection = 108, + tabCompletion = 109, + tabIndex = 110, + unusualLineTerminators = 111, + useShadowDOM = 112, + useTabStops = 113, + wordSeparators = 114, + wordWrap = 115, + wordWrapBreakAfterCharacters = 116, + wordWrapBreakBeforeCharacters = 117, + wordWrapColumn = 118, + wordWrapOverride1 = 119, + wordWrapOverride2 = 120, + wrappingIndent = 121, + wrappingStrategy = 122, + showDeprecated = 123, + inlayHints = 124, + editorClassName = 125, + pixelRatio = 126, + tabFocusMode = 127, + layoutInfo = 128, + wrappingInfo = 129 } export const EditorOptions: { acceptSuggestionOnCommitCharacter: IEditorOption; @@ -4217,12 +4250,13 @@ declare namespace monaco.editor { showFoldingControls: IEditorOption; showUnused: IEditorOption; showDeprecated: IEditorOption; - inlineHints: IEditorOption; + inlayHints: IEditorOption; snippetSuggestions: IEditorOption; smartSelect: IEditorOption; smoothScrolling: IEditorOption; stopRenderingLineAfter: IEditorOption; suggest: IEditorOption; + inlineSuggest: IEditorOption; suggestFontSize: IEditorOption; suggestLineHeight: IEditorOption; suggestOnTriggerCharacters: IEditorOption; @@ -4230,6 +4264,7 @@ declare namespace monaco.editor { tabCompletion: IEditorOption; tabIndex: IEditorOption; unusualLineTerminators: IEditorOption; + useShadowDOM: IEditorOption; useTabStops: IEditorOption; wordSeparators: IEditorOption; wordWrap: IEditorOption; @@ -4775,7 +4810,7 @@ declare namespace monaco.editor { getRawOptions(): IEditorOptions; /** * Get value of the current model attached to this editor. - * @see `ITextModel.getValue` + * @see {@link ITextModel.getValue} */ getValue(options?: { preserveBOM: boolean; @@ -4783,7 +4818,7 @@ declare namespace monaco.editor { }): string; /** * Set the value of the current model attached to this editor. - * @see `ITextModel.setValue` + * @see {@link ITextModel.setValue} */ setValue(newValue: string): void; /** @@ -4865,7 +4900,7 @@ declare namespace monaco.editor { getLineDecorations(lineNumber: number): IModelDecoration[] | null; /** * All decorations added through this call will get the ownerId of this editor. - * @see `ITextModel.deltaDecorations` + * @see {@link ITextModel.deltaDecorations} */ deltaDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[]; /** @@ -4970,7 +5005,7 @@ declare namespace monaco.editor { */ export interface IDiffEditor extends IEditor { /** - * @see ICodeEditor.getDomNode + * @see {@link ICodeEditor.getDomNode} */ getDomNode(): HTMLElement; /** @@ -5302,6 +5337,11 @@ declare namespace monaco.languages { */ export function registerDocumentRangeSemanticTokensProvider(languageId: string, provider: DocumentRangeSemanticTokensProvider): IDisposable; + /** + * Register an inline completions provider. + */ + export function registerInlineCompletionsProvider(languageId: string, provider: InlineCompletionsProvider): IDisposable; + /** * Contains additional diagnostic information about the context in which * a [code action](#CodeActionProvider.provideCodeActions) is run. @@ -5557,7 +5597,7 @@ declare namespace monaco.languages { } /** - * A provider result represents the values a provider, like the [`HoverProvider`](#HoverProvider), + * A provider result represents the values a provider, like the {@link HoverProvider}, * may return. For once this is the actual result type `T`, like `Hover`, or a thenable that resolves * to that type `T`. In addition, `null` and `undefined` can be returned - either directly or from a * thenable. @@ -5692,13 +5732,13 @@ declare namespace monaco.languages { documentation?: string | IMarkdownString; /** * A string that should be used when comparing this item - * with other items. When `falsy` the [label](#CompletionItem.label) + * with other items. When `falsy` the {@link CompletionItem.label label} * is used. */ sortText?: string; /** * A string that should be used when filtering a set of - * completion items. When `falsy` the [label](#CompletionItem.label) + * completion items. When `falsy` the {@link CompletionItem.label label} * is used. */ filterText?: string; @@ -5722,11 +5762,11 @@ declare namespace monaco.languages { /** * A range of text that should be replaced by this completion item. * - * Defaults to a range from the start of the [current word](#TextDocument.getWordRangeAtPosition) to the + * Defaults to a range from the start of the {@link TextDocument.getWordRangeAtPosition current word} to the * current position. * - * *Note:* The range must be a [single line](#Range.isSingleLine) and it must - * [contain](#Range.contains) the position at which completion has been [requested](#CompletionItemProvider.provideCompletionItems). + * *Note:* The range must be a {@link Range.isSingleLine single line} and it must + * {@link Range.contains contain} the position at which completion has been {@link CompletionItemProvider.provideCompletionItems requested}. */ range: IRange | { insert: IRange; @@ -5767,7 +5807,7 @@ declare namespace monaco.languages { /** * Contains additional information about the context in which - * [completion provider](#CompletionItemProvider.provideCompletionItems) is triggered. + * {@link CompletionItemProvider.provideCompletionItems completion provider} is triggered. */ export interface CompletionContext { /** @@ -5788,10 +5828,10 @@ declare namespace monaco.languages { * * When computing *complete* completion items is expensive, providers can optionally implement * the `resolveCompletionItem`-function. In that case it is enough to return completion - * items with a [label](#CompletionItem.label) from the - * [provideCompletionItems](#CompletionItemProvider.provideCompletionItems)-function. Subsequently, + * items with a {@link CompletionItem.label label} from the + * {@link CompletionItemProvider.provideCompletionItems provideCompletionItems}-function. Subsequently, * when a completion item is shown in the UI and gains focus this provider is asked to resolve - * the item, like adding [doc-comment](#CompletionItem.documentation) or [details](#CompletionItem.detail). + * the item, like adding {@link CompletionItem.documentation doc-comment} or {@link CompletionItem.detail details}. */ export interface CompletionItemProvider { triggerCharacters?: string[]; @@ -5800,14 +5840,68 @@ declare namespace monaco.languages { */ provideCompletionItems(model: editor.ITextModel, position: Position, context: CompletionContext, token: CancellationToken): ProviderResult; /** - * Given a completion item fill in more data, like [doc-comment](#CompletionItem.documentation) - * or [details](#CompletionItem.detail). + * Given a completion item fill in more data, like {@link CompletionItem.documentation doc-comment} + * or {@link CompletionItem.detail details}. * * The editor will only resolve a completion item once. */ resolveCompletionItem?(item: CompletionItem, token: CancellationToken): ProviderResult; } + /** + * How an {@link InlineCompletionsProvider inline completion provider} was triggered. + */ + export enum InlineCompletionTriggerKind { + /** + * Completion was triggered automatically while editing. + * It is sufficient to return a single completion item in this case. + */ + Automatic = 0, + /** + * Completion was triggered explicitly by a user gesture. + * Return multiple completion items to enable cycling through them. + */ + Explicit = 1 + } + + export interface InlineCompletionContext { + /** + * How the completion was triggered. + */ + readonly triggerKind: InlineCompletionTriggerKind; + } + + export interface InlineCompletion { + /** + * The text to insert. + * If the text contains a line break, the range must end at the end of a line. + * If existing text should be replaced, the existing text must be a prefix of the text to insert. + */ + readonly text: string; + /** + * The range to replace. + * Must begin and end on the same line. + */ + readonly range?: IRange; + readonly command?: Command; + } + + export interface InlineCompletions { + readonly items: readonly TItem[]; + } + + export interface InlineCompletionsProvider { + provideInlineCompletions(model: editor.ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult; + /** + * Will be called when an item is shown. + */ + handleItemDidShow?(completions: T, item: T['items'][number]): void; + /** + * Will be called when a completions list is no longer in use and can be garbage-collected. + */ + freeInlineCompletions(completions: T): void; + } + export interface CodeAction { title: string; command?: Command; @@ -5946,7 +6040,7 @@ declare namespace monaco.languages { */ range: IRange; /** - * The highlight kind, default is [text](#DocumentHighlightKind.Text). + * The highlight kind, default is {@link DocumentHighlightKind.Text text}. */ kind?: DocumentHighlightKind; } @@ -6273,12 +6367,12 @@ declare namespace monaco.languages { */ label: string; /** - * An [edit](#TextEdit) which is applied to a document when selecting + * An {@link TextEdit edit} which is applied to a document when selecting * this presentation for the color. */ textEdit?: TextEdit; /** - * An optional array of additional [text edits](#TextEdit) that are applied when + * An optional array of additional {@link TextEdit text edits} that are applied when * selecting this color presentation. */ additionalTextEdits?: TextEdit[]; @@ -6350,10 +6444,10 @@ declare namespace monaco.languages { */ end: number; /** - * Describes the [Kind](#FoldingRangeKind) of the folding range such as [Comment](#FoldingRangeKind.Comment) or - * [Region](#FoldingRangeKind.Region). The kind is used to categorize folding ranges and used by commands + * Describes the {@link FoldingRangeKind Kind} of the folding range such as {@link FoldingRangeKind.Comment Comment} or + * {@link FoldingRangeKind.Region Region}. The kind is used to categorize folding ranges and used by commands * like 'Fold all comments'. See - * [FoldingRangeKind](#FoldingRangeKind) for an enumeration of standardized kinds. + * {@link FoldingRangeKind} for an enumeration of standardized kinds. */ kind?: FoldingRangeKind; } @@ -6374,7 +6468,7 @@ declare namespace monaco.languages { */ static readonly Region: FoldingRangeKind; /** - * Creates a new [FoldingRangeKind](#FoldingRangeKind). + * Creates a new {@link FoldingRangeKind}. * * @param value of the kind. */ @@ -6454,24 +6548,23 @@ declare namespace monaco.languages { resolveCodeLens?(model: editor.ITextModel, codeLens: CodeLens, token: CancellationToken): ProviderResult; } - export enum InlineHintKind { + export enum InlayHintKind { Other = 0, Type = 1, Parameter = 2 } - export interface InlineHint { + export interface InlayHint { text: string; - range: IRange; - kind: InlineHintKind; - description?: string | IMarkdownString; + position: IPosition; + kind: InlayHintKind; whitespaceBefore?: boolean; whitespaceAfter?: boolean; } - export interface InlineHintsProvider { - onDidChangeInlineHints?: IEvent | undefined; - provideInlineHints(model: editor.ITextModel, range: Range, token: CancellationToken): ProviderResult; + export interface InlayHintsProvider { + onDidChangeInlayHints?: IEvent | undefined; + provideInlayHints(model: editor.ITextModel, range: Range, token: CancellationToken): ProviderResult; } export interface SemanticTokensLegend { diff --git a/src/vs/base/browser/ui/dropdown/dropdownWithPrimaryActionViewItem.ts b/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts similarity index 63% rename from src/vs/base/browser/ui/dropdown/dropdownWithPrimaryActionViewItem.ts rename to src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts index eef52a5a0e..228998d780 100644 --- a/src/vs/base/browser/ui/dropdown/dropdownWithPrimaryActionViewItem.ts +++ b/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts @@ -1,40 +1,52 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IContextMenuProvider } from 'vs/base/browser/contextmenu'; +import * as DOM from 'vs/base/browser/dom'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionViewItem, BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; import { IAction } from 'vs/base/common/actions'; -import * as DOM from 'vs/base/browser/dom'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { MenuItemAction } from 'vs/platform/actions/common/actions'; +import { MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { INotificationService } from 'vs/platform/notification/common/notification'; export class DropdownWithPrimaryActionViewItem extends BaseActionViewItem { private _primaryAction: ActionViewItem; private _dropdown: DropdownMenuActionViewItem; private _container: HTMLElement | null = null; - private toDispose: IDisposable[]; + private _dropdownContainer: HTMLElement | null = null; + + get onDidChangeDropdownVisibility(): Event { + return this._dropdown.onDidChangeVisibility; + } constructor( - primaryAction: IAction, + primaryAction: MenuItemAction, dropdownAction: IAction, dropdownMenuActions: IAction[], - _className: string, + className: string, private readonly _contextMenuProvider: IContextMenuProvider, - dropdownIcon?: string + _keybindingService: IKeybindingService, + _notificationService: INotificationService ) { super(null, primaryAction); - this._primaryAction = new ActionViewItem(undefined, primaryAction, { - icon: true, - label: false - }); + this._primaryAction = new MenuEntryActionViewItem(primaryAction, _keybindingService, _notificationService); this._dropdown = new DropdownMenuActionViewItem(dropdownAction, dropdownMenuActions, this._contextMenuProvider, { - menuAsChild: true + menuAsChild: true, + classNames: ['codicon', 'codicon-chevron-down'] }); - this.toDispose = []; + } + + override setActionContext(newContext: unknown): void { + super.setActionContext(newContext); + this._primaryAction.setActionContext(newContext); + this._dropdown.setActionContext(newContext); } override render(container: HTMLElement): void { @@ -43,10 +55,9 @@ export class DropdownWithPrimaryActionViewItem extends BaseActionViewItem { this._container.classList.add('monaco-dropdown-with-primary'); const primaryContainer = DOM.$('.action-container'); this._primaryAction.render(DOM.append(this._container, primaryContainer)); - const dropdownContainer = DOM.$('.dropdown-action-container'); - this._dropdown.render(DOM.append(this._container, dropdownContainer)); - - this.toDispose.push(DOM.addDisposableListener(primaryContainer, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => { + this._dropdownContainer = DOM.$('.dropdown-action-container'); + this._dropdown.render(DOM.append(this._container, this._dropdownContainer)); + this._register(DOM.addDisposableListener(primaryContainer, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => { const event = new StandardKeyboardEvent(e); if (event.equals(KeyCode.RightArrow)) { this._primaryAction.element!.tabIndex = -1; @@ -54,7 +65,7 @@ export class DropdownWithPrimaryActionViewItem extends BaseActionViewItem { event.stopPropagation(); } })); - this.toDispose.push(DOM.addDisposableListener(dropdownContainer, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => { + this._register(DOM.addDisposableListener(this._dropdownContainer, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => { const event = new StandardKeyboardEvent(e); if (event.equals(KeyCode.LeftArrow)) { this._primaryAction.element!.tabIndex = 0; @@ -89,18 +100,20 @@ export class DropdownWithPrimaryActionViewItem extends BaseActionViewItem { } } - override dispose(): void { - this.toDispose = dispose(this.toDispose); - } - update(dropdownAction: IAction, dropdownMenuActions: IAction[], dropdownIcon?: string): void { - this._dropdown?.dispose(); + this._dropdown.dispose(); this._dropdown = new DropdownMenuActionViewItem(dropdownAction, dropdownMenuActions, this._contextMenuProvider, { menuAsChild: true, classNames: ['codicon', dropdownIcon || 'codicon-chevron-down'] }); - if (this.element) { - this._dropdown.render(this.element); + if (this._dropdownContainer) { + this._dropdown.render(this._dropdownContainer); } } + + override dispose() { + this._primaryAction.dispose(); + this._dropdown.dispose(); + super.dispose(); + } } diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index d994b48572..eec96d2aca 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -24,14 +24,16 @@ export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuAct const groups = menu.getActions(options); const modifierKeyEmitter = ModifierKeyEmitter.getInstance(); const useAlternativeActions = modifierKeyEmitter.keyStatus.altKey || ((isWindows || isLinux) && modifierKeyEmitter.keyStatus.shiftKey); - fillInActions(groups, target, useAlternativeActions, primaryGroup); + fillInActions(groups, target, useAlternativeActions, primaryGroup ? actionGroup => actionGroup === primaryGroup : actionGroup => actionGroup === 'navigation'); return asDisposable(groups); } -export function createAndFillInActionBarActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, primaryGroup?: string, primaryMaxCount?: number, shouldInlineSubmenu?: (action: SubmenuAction, group: string, groupSize: number) => boolean): IDisposable { +export function createAndFillInActionBarActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, primaryGroup?: string | ((actionGroup: string) => boolean), primaryMaxCount?: number, shouldInlineSubmenu?: (action: SubmenuAction, group: string, groupSize: number) => boolean, useSeparatorsInPrimaryActions?: boolean): IDisposable { const groups = menu.getActions(options); + const isPrimaryAction = typeof primaryGroup === 'string' ? (actionGroup: string) => actionGroup === primaryGroup : primaryGroup; + // Action bars handle alternative actions on their own so the alternative actions should be ignored - fillInActions(groups, target, false, primaryGroup, primaryMaxCount, shouldInlineSubmenu); + fillInActions(groups, target, false, isPrimaryAction, primaryMaxCount, shouldInlineSubmenu, useSeparatorsInPrimaryActions); return asDisposable(groups); } @@ -48,9 +50,10 @@ function asDisposable(groups: ReadonlyArray<[string, ReadonlyArray]>, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, useAlternativeActions: boolean, - primaryGroup = 'navigation', + isPrimaryAction: (actionGroup: string) => boolean = actionGroup => actionGroup === 'navigation', primaryMaxCount: number = Number.MAX_SAFE_INTEGER, - shouldInlineSubmenu: (action: SubmenuAction, group: string, groupSize: number) => boolean = () => false + shouldInlineSubmenu: (action: SubmenuAction, group: string, groupSize: number) => boolean = () => false, + useSeparatorsInPrimaryActions: boolean = false ): void { let primaryBucket: IAction[]; @@ -68,8 +71,11 @@ export function fillInActions( // {{SQL CARBON EDIT}} add export modifier for (const [group, actions] of groups) { let target: IAction[]; - if (group === primaryGroup) { + if (isPrimaryAction(group)) { target = primaryBucket; + if (target.length > 0 && useSeparatorsInPrimaryActions) { + target.push(new Separator()); + } } else { target = secondaryBucket; if (target.length > 0) { @@ -92,7 +98,7 @@ export function fillInActions( // {{SQL CARBON EDIT}} add export modifier // ask the outside if submenu should be inlined or not. only ask when // there would be enough space for (const { group, action, index } of submenuInfo) { - const target = group === primaryGroup ? primaryBucket : secondaryBucket; + const target = isPrimaryAction(group) ? primaryBucket : secondaryBucket; // inlining submenus with length 0 or 1 is easy, // larger submenus need to be checked with the overall limit @@ -132,13 +138,15 @@ export class MenuEntryActionViewItem extends ActionViewItem { return this._wantsAltCommand && this._menuItemAction.alt || this._menuItemAction; } - override onClick(event: MouseEvent): void { + override async onClick(event: MouseEvent): Promise { event.preventDefault(); event.stopPropagation(); - this.actionRunner - .run(this._commandAction, this._context) - .catch(err => this._notificationService.error(err)); + try { + await this.actionRunner.run(this._commandAction, this._context); + } catch (err) { + this._notificationService.error(err); + } } override render(container: HTMLElement): void { diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 57b860a45c..75ba99aa0b 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -41,6 +41,7 @@ export type Icon = { dark?: URI; light?: URI; } | ThemeIcon; export interface ICommandAction { id: string; title: string | ICommandActionTitle; + shortTitle?: string | ICommandActionTitle; category?: string | ILocalizedString; tooltip?: string; icon?: Icon; @@ -87,6 +88,7 @@ export class MenuId { static readonly DebugWatchContext = new MenuId('DebugWatchContext'); static readonly DebugToolBar = new MenuId('DebugToolBar'); static readonly EditorContext = new MenuId('EditorContext'); + static readonly SimpleEditorContext = new MenuId('SimpleEditorContext'); static readonly EditorContextCopy = new MenuId('EditorContextCopy'); static readonly EditorContextPeek = new MenuId('EditorContextPeek'); static readonly EditorTitle = new MenuId('EditorTitle'); @@ -96,6 +98,7 @@ export class MenuId { static readonly ExplorerContext = new MenuId('ExplorerContext'); static readonly ExtensionContext = new MenuId('ExtensionContext'); static readonly GlobalActivity = new MenuId('GlobalActivity'); + static readonly MenubarMainMenu = new MenuId('MenubarMainMenu'); static readonly MenubarAppearanceMenu = new MenuId('MenubarAppearanceMenu'); static readonly MenubarDebugMenu = new MenuId('MenubarDebugMenu'); static readonly MenubarEditMenu = new MenuId('MenubarEditMenu'); @@ -125,9 +128,12 @@ export class MenuId { static readonly StatusBarWindowIndicatorMenu = new MenuId('StatusBarWindowIndicatorMenu'); static readonly StatusBarRemoteIndicatorMenu = new MenuId('StatusBarRemoteIndicatorMenu'); static readonly TestItem = new MenuId('TestItem'); + static readonly TestPeekElement = new MenuId('TestPeekElement'); + static readonly TestPeekTitle = new MenuId('TestPeekTitle'); static readonly TouchBarContext = new MenuId('TouchBarContext'); static readonly TitleBarContext = new MenuId('TitleBarContext'); static readonly TunnelContext = new MenuId('TunnelContext'); + static readonly TunnelProtocol = new MenuId('TunnelProtocol'); static readonly TunnelPortInline = new MenuId('TunnelInline'); static readonly TunnelTitle = new MenuId('TunnelTitle'); static readonly TunnelLocalAddressInline = new MenuId('TunnelLocalAddressInline'); @@ -142,6 +148,7 @@ export class MenuId { static readonly CommentTitle = new MenuId('CommentTitle'); static readonly CommentActions = new MenuId('CommentActions'); // static readonly NotebookToolbar = new MenuId('NotebookToolbar'); {{SQL CARBON EDIT}} We have our own toolbar + static readonly NotebookRightToolbar = new MenuId('NotebookRightToolbar'); static readonly NotebookCellTitle = new MenuId('NotebookCellTitle'); static readonly NotebookCellInsert = new MenuId('NotebookCellInsert'); static readonly NotebookCellBetween = new MenuId('NotebookCellBetween'); @@ -150,6 +157,7 @@ export class MenuId { static readonly NotebookDiffCellInputTitle = new MenuId('NotebookDiffCellInputTitle'); static readonly NotebookDiffCellMetadataTitle = new MenuId('NotebookDiffCellMetadataTitle'); static readonly NotebookDiffCellOutputsTitle = new MenuId('NotebookDiffCellOutputsTitle'); + static readonly NotebookOutputToolbar = new MenuId('NotebookOutputToolbar'); static readonly BulkEditTitle = new MenuId('BulkEditTitle'); static readonly BulkEditContext = new MenuId('BulkEditContext'); static readonly ObjectExplorerItemContext = new MenuId('ObjectExplorerItemContext'); // {{SQL CARBON EDIT}} @@ -166,12 +174,13 @@ export class MenuId { static readonly TimelineTitleContext = new MenuId('TimelineTitleContext'); static readonly AccountsContext = new MenuId('AccountsContext'); static readonly PanelTitle = new MenuId('PanelTitle'); - static readonly TerminalContainerContext = new MenuId('TerminalContainerContext'); - static readonly TerminalToolbarContext = new MenuId('TerminalToolbarContext'); - static readonly TerminalTabsWidgetContext = new MenuId('TerminalTabsWidgetContext'); - static readonly TerminalTabsWidgetEmptyContext = new MenuId('TerminalTabsWidgetEmptyContext'); - static readonly TerminalSingleTabContext = new MenuId('TerminalSingleTabContext'); - static readonly TerminalTabInlineActions = new MenuId('TerminalTabInlineActions'); + static readonly TerminalInstanceContext = new MenuId('TerminalInstanceContext'); + static readonly TerminalNewDropdownContext = new MenuId('TerminalNewDropdownContext'); + static readonly TerminalTabContext = new MenuId('TerminalTabContext'); + static readonly TerminalTabEmptyAreaContext = new MenuId('TerminalTabEmptyAreaContext'); + static readonly TerminalInlineTabContext = new MenuId('TerminalInlineTabContext'); + static readonly WebviewContext = new MenuId('WebviewContext'); + static readonly InlineCompletionsActions = new MenuId('InlineCompletionsActions'); readonly id: number; readonly _debugName: string; @@ -185,6 +194,7 @@ export class MenuId { export interface IMenuActionOptions { arg?: any; shouldForwardArgs?: boolean; + renderShortTitle?: boolean; } export interface IMenu extends IDisposable { @@ -213,7 +223,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; } @@ -264,7 +274,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(); @@ -336,7 +346,7 @@ export class ExecuteCommandAction extends Action { super(id, label); } - override run(...args: any[]): Promise { + override run(...args: any[]): Promise { return this._commandService.executeCommand(this.id, ...args); } } @@ -386,7 +396,7 @@ export class MenuItemAction implements IAction { readonly class: string | undefined; readonly enabled: boolean; readonly checked: boolean; - readonly expanded: boolean = false; + readonly expanded: boolean = false; // {{SQL CARBON EDIT}} constructor( item: ICommandAction, @@ -396,7 +406,9 @@ export class MenuItemAction implements IAction { @ICommandService private _commandService: ICommandService ) { this.id = item.id; - this.label = typeof item.title === 'string' ? item.title : item.title.value; + this.label = options?.renderShortTitle && item.shortTitle + ? (typeof item.shortTitle === 'string' ? item.shortTitle : item.shortTitle.value) + : (typeof item.title === 'string' ? item.title : item.title.value); this.tooltip = item.tooltip ?? ''; this.enabled = !item.precondition || contextKeyService.contextMatchesRules(item.precondition); this.checked = false; @@ -429,7 +441,7 @@ export class MenuItemAction implements IAction { // to bridge into the rendering world. } - run(...args: any[]): Promise { + run(...args: any[]): Promise { let runArgs: any[] = []; if (this._options?.arg) { @@ -442,8 +454,6 @@ export class MenuItemAction implements IAction { return this._commandService.executeCommand(this.id, ...runArgs); } - - } export class SyncActionDescriptor { @@ -456,7 +466,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); @@ -523,7 +533,7 @@ export interface IAction2Options extends ICommandAction { /** * One or many menu items. */ - menu?: OneOrN<{ id: MenuId; } & Omit>; + menu?: OneOrN<{ id: MenuId } & Omit>; /** * One keybinding. @@ -539,10 +549,10 @@ export interface IAction2Options extends ICommandAction { export abstract class Action2 { constructor(readonly desc: Readonly) { } - abstract run(accessor: ServicesAccessor, ...args: any[]): any; + abstract run(accessor: ServicesAccessor, ...args: any[]): void; } -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 52f17db5b1..c7f6f09385 100644 --- a/src/vs/platform/actions/common/menuService.ts +++ b/src/vs/platform/actions/common/menuService.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { RunOnceScheduler } from 'vs/base/common/async'; @@ -116,7 +116,7 @@ class Menu implements IMenu { } // keep toggled keys for event if applicable if (item.command.toggled) { - const toggledExpression: ContextKeyExpression = (item.command.toggled as { condition: ContextKeyExpression }).condition || item.command.toggled as ContextKeyExpression; + const toggledExpression: ContextKeyExpression = (item.command.toggled as { condition: ContextKeyExpression }).condition || item.command.toggled as ContextKeyExpression; // {{SQL CARBON EDIT}} Cast to ContextKeyExpression Menu._fillInKbExprKeys(toggledExpression, this._contextKeys); } diff --git a/src/vs/platform/backup/electron-main/backupMainService.ts b/src/vs/platform/backup/electron-main/backupMainService.ts index 523407e5c2..1f1df054e7 100644 --- a/src/vs/platform/backup/electron-main/backupMainService.ts +++ b/src/vs/platform/backup/electron-main/backupMainService.ts @@ -7,7 +7,7 @@ import * as fs from 'fs'; 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 { writeFileSync, RimRafMode, Promises } 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'; @@ -49,26 +49,28 @@ export class BackupMainService implements IBackupMainService { async initialize(): Promise { let backups: IBackupWorkspacesFormat; try { - backups = JSON.parse(await fs.promises.readFile(this.workspacesJsonPath, 'utf8')); // invalid JSON or permission issue can happen here + backups = JSON.parse(await Promises.readFile(this.workspacesJsonPath, 'utf8')); // invalid JSON or permission issue can happen here } catch (error) { backups = Object.create(null); } - // read empty workspaces backups first - if (backups.emptyWorkspaceInfos) { - this.emptyWindows = await this.validateEmptyWorkspaces(backups.emptyWorkspaceInfos); - } + // validate empty workspaces backups first + this.emptyWindows = await this.validateEmptyWorkspaces(backups.emptyWorkspaceInfos); // read workspace backups let rootWorkspaces: IWorkspaceBackupInfo[] = []; try { if (Array.isArray(backups.rootURIWorkspaces)) { - rootWorkspaces = backups.rootURIWorkspaces.map(workspace => ({ workspace: { id: workspace.id, configPath: URI.parse(workspace.configURIPath) }, remoteAuthority: workspace.remoteAuthority })); + rootWorkspaces = backups.rootURIWorkspaces.map(workspace => ({ + workspace: { id: workspace.id, configPath: URI.parse(workspace.configURIPath) }, + remoteAuthority: workspace.remoteAuthority + })); } } catch (e) { // ignore URI parsing exceptions } + // validate workspace backups this.workspaces = await this.validateWorkspaces(rootWorkspaces); // read folder backups @@ -81,6 +83,7 @@ export class BackupMainService implements IBackupMainService { // ignore URI parsing exceptions } + // validate folder backups this.folders = await this.validateFolders(workspaceFolders); // save again in case some workspaces or folders have been removed @@ -230,7 +233,7 @@ export class BackupMainService implements IBackupMainService { // If the workspace has no backups, ignore it if (hasBackups) { - if (workspace.configPath.scheme !== Schemas.file || await exists(workspace.configPath.fsPath)) { + if (workspace.configPath.scheme !== Schemas.file || await Promises.exists(workspace.configPath.fsPath)) { result.push(workspaceInfo); } else { // If the workspace has backups, but the target workspace is missing, convert backups to empty ones @@ -262,7 +265,7 @@ export class BackupMainService implements IBackupMainService { // If the folder has no backups, ignore it if (hasBackups) { - if (folderURI.scheme !== Schemas.file || await exists(folderURI.fsPath)) { + if (folderURI.scheme !== Schemas.file || await Promises.exists(folderURI.fsPath)) { result.push(folderURI); } else { // If the folder has backups, but the target workspace is missing, convert backups to empty ones @@ -309,8 +312,8 @@ export class BackupMainService implements IBackupMainService { private async deleteStaleBackup(backupPath: string): Promise { try { - if (await exists(backupPath)) { - await rimraf(backupPath, RimRafMode.MOVE); + if (await Promises.exists(backupPath)) { + await Promises.rm(backupPath, RimRafMode.MOVE); } } catch (error) { this.logService.error(`Backup: Could not delete stale backup: ${error.toString()}`); @@ -328,7 +331,7 @@ export class BackupMainService implements IBackupMainService { // Rename backupPath to new empty window backup path const newEmptyWindowBackupPath = this.getBackupPath(newBackupFolder); try { - await fs.promises.rename(backupPath, newEmptyWindowBackupPath); + await Promises.rename(backupPath, newEmptyWindowBackupPath); } catch (error) { this.logService.error(`Backup: Could not rename backup folder: ${error.toString()}`); return false; @@ -402,11 +405,11 @@ export class BackupMainService implements IBackupMainService { private async doHasBackups(backupPath: string): Promise { try { - const backupSchemas = await readdir(backupPath); + const backupSchemas = await Promises.readdir(backupPath); for (const backupSchema of backupSchemas) { try { - const backupSchemaChildren = await readdir(join(backupPath, backupSchema)); + const backupSchemaChildren = await Promises.readdir(join(backupPath, backupSchema)); if (backupSchemaChildren.length > 0) { return true; } @@ -431,7 +434,7 @@ export class BackupMainService implements IBackupMainService { private async save(): Promise { try { - await writeFile(this.workspacesJsonPath, JSON.stringify(this.serializeBackups())); + await Promises.writeFile(this.workspacesJsonPath, JSON.stringify(this.serializeBackups())); } catch (error) { this.logService.error(`Backup: Could not save workspaces.json: ${error.toString()}`); } 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 a35c4f4602..a1582742f0 100644 --- a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts +++ b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts @@ -66,7 +66,7 @@ flakySuite('BackupMainService', () => { async function ensureWorkspaceExists(workspace: IWorkspaceIdentifier): Promise { if (!fs.existsSync(workspace.configPath.fsPath)) { - await pfs.writeFile(workspace.configPath.fsPath, 'Hello'); + await pfs.Promises.writeFile(workspace.configPath.fsPath, 'Hello'); } const backupFolder = service.toBackupPath(workspace.id); @@ -79,7 +79,7 @@ flakySuite('BackupMainService', () => { if (!fs.existsSync(backupFolder)) { fs.mkdirSync(backupFolder); fs.mkdirSync(path.join(backupFolder, Schemas.file)); - await pfs.writeFile(path.join(backupFolder, Schemas.file, 'foo.txt'), 'Hello'); + await pfs.Promises.writeFile(path.join(backupFolder, Schemas.file, 'foo.txt'), 'Hello'); } } @@ -107,7 +107,7 @@ flakySuite('BackupMainService', () => { environmentService = new EnvironmentMainService(parseArgs(process.argv, OPTIONS), { _serviceBrand: undefined, ...product }); - await fs.promises.mkdir(backupHome, { recursive: true }); + await pfs.Promises.mkdir(backupHome, { recursive: true }); configService = new TestConfigurationService(); service = new class TestBackupMainService extends BackupMainService { @@ -132,7 +132,7 @@ flakySuite('BackupMainService', () => { }); teardown(() => { - return pfs.rimraf(testDir); + return pfs.Promises.rm(testDir); }); test('service validates backup workspaces on startup and cleans up (folder workspaces)', async function () { @@ -443,10 +443,10 @@ flakySuite('BackupMainService', () => { folderURIWorkspaces: [existingTestFolder1.toString(), existingTestFolder1.toString()], emptyWorkspaceInfos: [] }; - await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)); + await pfs.Promises.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)); await service.initialize(); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); @@ -460,9 +460,9 @@ flakySuite('BackupMainService', () => { folderURIWorkspaces: [existingTestFolder1.toString(), existingTestFolder1.toString().toLowerCase()], emptyWorkspaceInfos: [] }; - await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)); + await pfs.Promises.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)); await service.initialize(); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); @@ -481,10 +481,10 @@ flakySuite('BackupMainService', () => { folderURIWorkspaces: [], emptyWorkspaceInfos: [] }; - await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)); + await pfs.Promises.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)); await service.initialize(); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.strictEqual(json.rootURIWorkspaces.length, platform.isLinux ? 3 : 1); if (platform.isLinux) { @@ -500,7 +500,7 @@ flakySuite('BackupMainService', () => { service.registerFolderBackupSync(fooFile); service.registerFolderBackupSync(barFile); assertEqualUris(service.getFolderBackupPaths(), [fooFile, barFile]); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.deepStrictEqual(json.folderURIWorkspaces, [fooFile.toString(), barFile.toString()]); }); @@ -515,7 +515,7 @@ flakySuite('BackupMainService', () => { assert.strictEqual(ws1.workspace.id, service.getWorkspaceBackups()[0].workspace.id); assert.strictEqual(ws2.workspace.id, service.getWorkspaceBackups()[1].workspace.id); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.deepStrictEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [fooFile.toString(), barFile.toString()]); @@ -528,7 +528,7 @@ flakySuite('BackupMainService', () => { service.registerFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase())); assertEqualUris(service.getFolderBackupPaths(), [URI.file(fooFile.fsPath.toUpperCase())]); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.deepStrictEqual(json.folderURIWorkspaces, [URI.file(fooFile.fsPath.toUpperCase()).toString()]); }); @@ -538,7 +538,7 @@ flakySuite('BackupMainService', () => { service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(upperFooPath)); assertEqualUris(service.getWorkspaceBackups().map(b => b.workspace.configPath), [URI.file(upperFooPath)]); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); assert.deepStrictEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [URI.file(upperFooPath).toString()]); }); @@ -549,12 +549,12 @@ flakySuite('BackupMainService', () => { service.registerFolderBackupSync(barFile); service.unregisterFolderBackupSync(fooFile); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); assert.deepStrictEqual(json.folderURIWorkspaces, [barFile.toString()]); service.unregisterFolderBackupSync(barFile); - const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const content = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json2 = (JSON.parse(content)); assert.deepStrictEqual(json2.folderURIWorkspaces, []); }); @@ -566,12 +566,12 @@ flakySuite('BackupMainService', () => { service.registerWorkspaceBackupSync(ws2); service.unregisterWorkspaceBackupSync(ws1.workspace); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); assert.deepStrictEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [barFile.toString()]); service.unregisterWorkspaceBackupSync(ws2.workspace); - const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const content = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json2 = (JSON.parse(content)); assert.deepStrictEqual(json2.rootURIWorkspaces, []); }); @@ -581,12 +581,12 @@ flakySuite('BackupMainService', () => { service.registerEmptyWindowBackupSync('bar'); service.unregisterEmptyWindowBackupSync('foo'); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); assert.deepStrictEqual(json.emptyWorkspaceInfos, [{ backupFolder: 'bar' }]); service.unregisterEmptyWindowBackupSync('bar'); - const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const content = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json2 = (JSON.parse(content)); assert.deepStrictEqual(json2.emptyWorkspaceInfos, []); }); @@ -596,11 +596,11 @@ flakySuite('BackupMainService', () => { await ensureFolderExists(existingTestFolder1); // make sure backup folder exists, so the folder is not removed on loadSync const workspacesJson: IBackupWorkspacesFormat = { rootURIWorkspaces: [], folderURIWorkspaces: [existingTestFolder1.toString()], emptyWorkspaceInfos: [] }; - await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)); + await pfs.Promises.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)); await service.initialize(); service.unregisterFolderBackupSync(barFile); service.unregisterEmptyWindowBackupSync('test'); - const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const content = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(content)); assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); @@ -670,8 +670,8 @@ flakySuite('BackupMainService', () => { assert.strictEqual(((await service.getDirtyWorkspaces()).length), 0); try { - await fs.promises.mkdir(path.join(folderBackupPath, Schemas.file), { recursive: true }); - await fs.promises.mkdir(path.join(workspaceBackupPath, Schemas.untitled), { recursive: true }); + await pfs.Promises.mkdir(path.join(folderBackupPath, Schemas.file), { recursive: true }); + await pfs.Promises.mkdir(path.join(workspaceBackupPath, Schemas.untitled), { recursive: true }); } catch (error) { // ignore - folder might exist already } diff --git a/src/vs/platform/checksum/common/checksumService.ts b/src/vs/platform/checksum/common/checksumService.ts index 9e38de5d11..d88c0445bf 100644 --- a/src/vs/platform/checksum/common/checksumService.ts +++ b/src/vs/platform/checksum/common/checksumService.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/platform/checksum/node/checksumService.ts b/src/vs/platform/checksum/node/checksumService.ts index 262f54ea06..1de53dfe88 100644 --- a/src/vs/platform/checksum/node/checksumService.ts +++ b/src/vs/platform/checksum/node/checksumService.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { createHash } from 'crypto'; diff --git a/src/vs/platform/checksum/test/node/checksumService.test.ts b/src/vs/platform/checksum/test/node/checksumService.test.ts index 6cc41c1074..45213a46a4 100644 --- a/src/vs/platform/checksum/test/node/checksumService.test.ts +++ b/src/vs/platform/checksum/test/node/checksumService.test.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; diff --git a/src/vs/platform/configuration/common/configuration.ts b/src/vs/platform/configuration/common/configuration.ts index 6fd279e030..e84d805bdb 100644 --- a/src/vs/platform/configuration/common/configuration.ts +++ b/src/vs/platform/configuration/common/configuration.ts @@ -112,7 +112,7 @@ export interface IConfigurationService { updateValue(key: string, value: any, target: ConfigurationTarget): Promise; updateValue(key: string, value: any, overrides: IConfigurationOverrides, target: ConfigurationTarget, donotNotifyError?: boolean): Promise; - inspect(key: string, overrides?: IConfigurationOverrides): IConfigurationValue; + inspect(key: string, overrides?: IConfigurationOverrides): IConfigurationValue>; reloadConfiguration(target?: ConfigurationTarget | IWorkspaceFolder): Promise; diff --git a/src/vs/platform/contextkey/common/contextkey.ts b/src/vs/platform/contextkey/common/contextkey.ts index 2bea0ad594..e0b07af415 100644 --- a/src/vs/platform/contextkey/common/contextkey.ts +++ b/src/vs/platform/contextkey/common/contextkey.ts @@ -21,7 +21,6 @@ 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; diff --git a/src/vs/platform/contextkey/common/contextkeys.ts b/src/vs/platform/contextkey/common/contextkeys.ts index ad8f9cbadb..71c020c9e6 100644 --- a/src/vs/platform/contextkey/common/contextkeys.ts +++ b/src/vs/platform/contextkey/common/contextkeys.ts @@ -5,7 +5,7 @@ import { localize } from 'vs/nls'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { isMacintosh, isLinux, isWindows, isWeb } from 'vs/base/common/platform'; +import { isMacintosh, isLinux, isWindows, isWeb, isIOS } from 'vs/base/common/platform'; export const IsMacContext = new RawContextKey('isMac', isMacintosh, localize('isMac', "Whether the operating system is macOS")); export const IsLinuxContext = new RawContextKey('isLinux', isLinux, localize('isLinux', "Whether the operating system is Linux")); @@ -13,6 +13,7 @@ export const IsWindowsContext = new RawContextKey('isWindows', isWindow export const IsWebContext = new RawContextKey('isWeb', isWeb, localize('isWeb', "Whether the platform is a web browser")); export const IsMacNativeContext = new RawContextKey('isMacNative', isMacintosh && !isWeb, localize('isMacNative', "Whether the operating system is macOS on a non-browser platform")); +export const IsIOSContext = new RawContextKey('isIOS', isIOS, localize('isIOS', "Whether the operating system is IOS")); export const IsDevelopmentContext = new RawContextKey('isDevelopment', false, true); diff --git a/src/vs/platform/contextkey/test/browser/contextkey.test.ts b/src/vs/platform/contextkey/test/browser/contextkey.test.ts new file mode 100644 index 0000000000..0262c18102 --- /dev/null +++ b/src/vs/platform/contextkey/test/browser/contextkey.test.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService'; +import * as assert from 'assert'; + +suite('ContextKeyService', () => { + test('updateParent', () => { + const root = new ContextKeyService(new TestConfigurationService()); + const parent1 = root.createScoped(document.createElement('div')); + const parent2 = root.createScoped(document.createElement('div')); + + const child = parent1.createScoped(document.createElement('div')); + parent1.createKey('testA', 1); + parent1.createKey('testB', 2); + parent1.createKey('testD', 0); + + parent2.createKey('testA', 3); + parent2.createKey('testC', 4); + parent2.createKey('testD', 0); + + let complete: () => void; + let reject: (err: Error) => void; + const p = new Promise((_complete, _reject) => { + complete = _complete; + reject = _reject; + }); + child.onDidChangeContext(e => { + try { + assert.ok(e.affectsSome(new Set(['testA'])), 'testA changed'); + assert.ok(e.affectsSome(new Set(['testB'])), 'testB changed'); + assert.ok(e.affectsSome(new Set(['testC'])), 'testC changed'); + assert.ok(!e.affectsSome(new Set(['testD'])), 'testD did not change'); + + assert.strictEqual(child.getContextKeyValue('testA'), 3); + assert.strictEqual(child.getContextKeyValue('testB'), undefined); + assert.strictEqual(child.getContextKeyValue('testC'), 4); + assert.strictEqual(child.getContextKeyValue('testD'), 0); + } catch (err) { + reject(err); + return; + } + + complete(); + }); + + child.updateParent(parent2); + + return p; + }); +}); diff --git a/src/vs/platform/contextkey/test/common/contextkey.test.ts b/src/vs/platform/contextkey/test/common/contextkey.test.ts index 3fb2369cc4..e52ec10b03 100644 --- a/src/vs/platform/contextkey/test/common/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/common/contextkey.test.ts @@ -27,7 +27,7 @@ suite('ContextKeyExpr', () => { ContextKeyExpr.notEquals('c1', 'cc1'), ContextKeyExpr.notEquals('c2', 'cc2'), ContextKeyExpr.not('d1'), - ContextKeyExpr.not('d2'), + ContextKeyExpr.not('d2') )!; let b = ContextKeyExpr.and( ContextKeyExpr.equals('b2', 'bb2'), diff --git a/src/vs/platform/contextview/browser/contextMenuService.ts b/src/vs/platform/contextview/browser/contextMenuService.ts index 5694d203e9..deb1cc6f05 100644 --- a/src/vs/platform/contextview/browser/contextMenuService.ts +++ b/src/vs/platform/contextview/browser/contextMenuService.ts @@ -12,12 +12,15 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Disposable } from 'vs/base/common/lifecycle'; import { ModifierKeyEmitter } from 'vs/base/browser/dom'; +import { Emitter } from 'vs/base/common/event'; export class ContextMenuService extends Disposable implements IContextMenuService { declare readonly _serviceBrand: undefined; private contextMenuHandler: ContextMenuHandler; + readonly onDidShowContextMenu = new Emitter().event; + constructor( @ITelemetryService telemetryService: ITelemetryService, @INotificationService notificationService: INotificationService, diff --git a/src/vs/platform/contextview/browser/contextView.ts b/src/vs/platform/contextview/browser/contextView.ts index 87b5f0d826..187b3e6922 100644 --- a/src/vs/platform/contextview/browser/contextView.ts +++ b/src/vs/platform/contextview/browser/contextView.ts @@ -7,6 +7,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IContextMenuDelegate } from 'vs/base/browser/contextmenu'; import { AnchorAlignment, AnchorAxisAlignment, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { Event } from 'vs/base/common/event'; export const IContextViewService = createDecorator('contextViewService'); @@ -40,5 +41,7 @@ export interface IContextMenuService { readonly _serviceBrand: undefined; + readonly onDidShowContextMenu: Event; + showContextMenu(delegate: IContextMenuDelegate): void; } diff --git a/src/vs/platform/diagnostics/electron-sandbox/diagnosticsService.ts b/src/vs/platform/diagnostics/electron-sandbox/diagnosticsService.ts index 45581940b5..92ff1ed4d2 100644 --- a/src/vs/platform/diagnostics/electron-sandbox/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/electron-sandbox/diagnosticsService.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { registerSharedProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services'; diff --git a/src/vs/platform/diagnostics/node/diagnosticsService.ts b/src/vs/platform/diagnostics/node/diagnosticsService.ts index 3edc17b68f..82197224f4 100644 --- a/src/vs/platform/diagnostics/node/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/node/diagnosticsService.ts @@ -5,7 +5,6 @@ import * as osLib from 'os'; import { virtualMachineHint } from 'vs/base/node/id'; import { IDiagnosticsService, IMachineInfo, WorkspaceStats, WorkspaceStatItem, PerformanceInfo, SystemInfo, IRemoteDiagnosticInfo, IRemoteDiagnosticError, isRemoteDiagnosticError, IWorkspaceInformation } from 'vs/platform/diagnostics/common/diagnostics'; -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'; @@ -18,7 +17,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; 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'; +import { IDirent, Promises } from 'vs/base/node/pfs'; export interface VersionInfo { vscodeVersion: string; @@ -70,7 +69,7 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P return new Promise(async resolve => { let files: IDirent[]; try { - files = await readdir(dir, { withFileTypes: true }); + files = await Promises.readdir(dir, { withFileTypes: true }); } catch (error) { // Ignore folders that can't be read resolve(); @@ -168,45 +167,37 @@ export function getMachineInfo(): IMachineInfo { return machineInfo; } -export function collectLaunchConfigs(folder: string): Promise { - let launchConfigs = new Map(); +export async function collectLaunchConfigs(folder: string): Promise { + try { + const launchConfigs = new Map(); + const launchConfig = join(folder, '.vscode', 'launch.json'); - let launchConfig = join(folder, '.vscode', 'launch.json'); - return new Promise((resolve, reject) => { - exists(launchConfig, (doesExist) => { - if (doesExist) { - readFile(launchConfig, (err, contents) => { - if (err) { - return resolve([]); + const contents = await Promises.readFile(launchConfig); + + const errors: ParseError[] = []; + const json = parse(contents.toString(), errors); + if (errors.length) { + console.log(`Unable to parse ${launchConfig}`); + return []; + } + + if (getNodeType(json) === 'object' && json['configurations']) { + for (const each of json['configurations']) { + const type = each['type']; + if (type) { + if (launchConfigs.has(type)) { + launchConfigs.set(type, launchConfigs.get(type)! + 1); + } else { + launchConfigs.set(type, 1); } - - const errors: ParseError[] = []; - const json = parse(contents.toString(), errors); - if (errors.length) { - console.log(`Unable to parse ${launchConfig}`); - return resolve([]); - } - - if (getNodeType(json) === 'object' && json['configurations']) { - for (const each of json['configurations']) { - const type = each['type']; - if (type) { - if (launchConfigs.has(type)) { - launchConfigs.set(type, launchConfigs.get(type)! + 1); - } else { - launchConfigs.set(type, 1); - } - } - } - } - - return resolve(asSortedItems(launchConfigs)); - }); - } else { - return resolve([]); + } } - }); - }); + } + + return asSortedItems(launchConfigs); + } catch (error) { + return []; + } } export class DiagnosticsService implements IDiagnosticsService { diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index 45e46b4873..eb8a3825e7 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -31,12 +31,13 @@ export interface IConfirmDialogArgs { export interface IShowDialogArgs { severity: Severity; message: string; - buttons: string[]; + buttons?: string[]; options?: IDialogOptions; } export interface IInputDialogArgs extends IShowDialogArgs { - inputs: IInput[], + buttons: string[]; + inputs: IInput[]; } export interface IDialog { @@ -222,7 +223,7 @@ export interface IDialogHandler { * then a promise with index of `cancelId` option is returned. If there is no such * option then promise with index `0` is returned. */ - show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise; + show(severity: Severity, message: string, buttons?: string[], options?: IDialogOptions): Promise; /** * Present a modal dialog to the user asking for input. @@ -262,7 +263,7 @@ export interface IDialogService { * then a promise with index of `cancelId` option is returned. If there is no such * option then promise with index `0` is returned. */ - show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise; + show(severity: Severity, message: string, buttons?: string[], options?: IDialogOptions): Promise; /** * Present a modal dialog to the user asking for input. diff --git a/src/vs/platform/dialogs/electron-main/dialogMainService.ts b/src/vs/platform/dialogs/electron-main/dialogMainService.ts index 3c7c7fa70e..b74a597157 100644 --- a/src/vs/platform/dialogs/electron-main/dialogMainService.ts +++ b/src/vs/platform/dialogs/electron-main/dialogMainService.ts @@ -6,11 +6,11 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { MessageBoxOptions, MessageBoxReturnValue, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, dialog, FileFilter, BrowserWindow } from 'electron'; import { Queue } from 'vs/base/common/async'; -import { IStateService } from 'vs/platform/state/node/state'; +import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { isMacintosh } from 'vs/base/common/platform'; import { dirname } from 'vs/base/common/path'; import { normalizeNFC } from 'vs/base/common/normalization'; -import { exists } from 'vs/base/node/pfs'; +import { Promises } from 'vs/base/node/pfs'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { withNullAsUndefined } from 'vs/base/common/types'; import { localize } from 'vs/nls'; @@ -55,7 +55,7 @@ export class DialogMainService implements IDialogMainService { private readonly noWindowDialogueQueue = new Queue(); constructor( - @IStateService private readonly stateService: IStateService + @IStateMainService private readonly stateMainService: IStateMainService ) { } @@ -89,8 +89,7 @@ export class DialogMainService implements IDialogMainService { }; // Ensure defaultPath - dialogOptions.defaultPath = options.defaultPath || this.stateService.getItem(DialogMainService.workingDirPickerStorageKey); - + dialogOptions.defaultPath = options.defaultPath || this.stateMainService.getItem(DialogMainService.workingDirPickerStorageKey); // Ensure properties if (typeof options.pickFiles === 'boolean' || typeof options.pickFolders === 'boolean') { @@ -116,12 +115,12 @@ export class DialogMainService implements IDialogMainService { if (result && result.filePaths && result.filePaths.length > 0) { // Remember path in storage for next time - this.stateService.setItem(DialogMainService.workingDirPickerStorageKey, dirname(result.filePaths[0])); + this.stateMainService.setItem(DialogMainService.workingDirPickerStorageKey, dirname(result.filePaths[0])); return result.filePaths; } - return undefined; + return undefined; // {{SQL CARBON EDIT}} Strict nulls } private getWindowDialogQueue(window?: BrowserWindow): Queue { @@ -195,7 +194,7 @@ export class DialogMainService implements IDialogMainService { // Ensure the path exists (if provided) if (options.defaultPath) { - const pathExists = await exists(options.defaultPath); + const pathExists = await Promises.exists(options.defaultPath); if (!pathExists) { options.defaultPath = undefined; } diff --git a/src/vs/platform/dialogs/test/common/testDialogService.ts b/src/vs/platform/dialogs/test/common/testDialogService.ts index b80ea3a1b2..eee527c50f 100644 --- a/src/vs/platform/dialogs/test/common/testDialogService.ts +++ b/src/vs/platform/dialogs/test/common/testDialogService.ts @@ -10,8 +10,23 @@ export class TestDialogService implements IDialogService { declare readonly _serviceBrand: undefined; - confirm(_confirmation: IConfirmation): Promise { return Promise.resolve({ confirmed: false }); } - show(_severity: Severity, _message: string, _buttons: string[], _options?: IDialogOptions): Promise { return Promise.resolve({ choice: 0 }); } - input(): Promise { { return Promise.resolve({ choice: 0, values: [] }); } } - about(): Promise { return Promise.resolve(); } + private confirmResult: IConfirmationResult | undefined = undefined; + setConfirmResult(result: IConfirmationResult) { + this.confirmResult = result; + } + + async confirm(confirmation: IConfirmation): Promise { + if (this.confirmResult) { + const confirmResult = this.confirmResult; + this.confirmResult = undefined; + + return confirmResult; + } + + return { confirmed: false }; + } + + async show(severity: Severity, message: string, buttons?: string[], options?: IDialogOptions): Promise { return { choice: 0 }; } + async input(): Promise { { return { choice: 0, values: [] }; } } + async about(): Promise { } } diff --git a/src/vs/platform/driver/common/driverIpc.ts b/src/vs/platform/driver/common/driverIpc.ts index f492817c1d..255f3299c4 100644 --- a/src/vs/platform/driver/common/driverIpc.ts +++ b/src/vs/platform/driver/common/driverIpc.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; diff --git a/src/vs/platform/editor/common/editor.ts b/src/vs/platform/editor/common/editor.ts index 8401717c8a..b77a913af2 100644 --- a/src/vs/platform/editor/common/editor.ts +++ b/src/vs/platform/editor/common/editor.ts @@ -37,17 +37,17 @@ export interface IEditorModel { export interface IBaseResourceEditorInput { /** - * Optional options to use when opening the text input. + * Optional options to use when opening the input. */ - options?: ITextEditorOptions; + options?: IEditorOptions; /** - * Label to show for the diff editor + * Label to show for the input. */ readonly label?: string; /** - * Description to show for the diff editor + * Description to show for the input. */ readonly description?: string; @@ -70,6 +70,48 @@ export interface IBaseResourceEditorInput { readonly forceUntitled?: boolean; } +export interface IBaseTextResourceEditorInput extends IBaseResourceEditorInput { + + /** + * Optional options to use when opening the text input. + */ + options?: ITextEditorOptions; + + /** + * The contents of the text input if known. If provided, + * the input will not attempt to load the contents from + * disk and may appear dirty. + */ + contents?: string; + + /** + * The encoding of the text input if known. + */ + encoding?: string; + + /** + * The identifier of the language mode of the text input + * if known to use when displaying the contents. + */ + mode?: string; +} + +export interface IResourceEditorInput extends IBaseResourceEditorInput { + + /** + * The resource URI of the resource to open. + */ + readonly resource: URI; +} + +export interface ITextResourceEditorInput extends IResourceEditorInput, IBaseTextResourceEditorInput { + + /** + * Optional options to use when opening the text input. + */ + options?: ITextEditorOptions; +} + /** * This identifier allows to uniquely identify an editor with a * resource and type identifier. @@ -87,25 +129,6 @@ export interface IResourceEditorInputIdentifier { readonly typeId: string; } -export interface IResourceEditorInput extends IBaseResourceEditorInput { - - /** - * The resource URI of the resource to open. - */ - readonly resource: URI; - - /** - * The encoding of the text input if known. - */ - readonly encoding?: string; - - /** - * The identifier of the language mode of the text input - * if known to use when displaying the contents. - */ - readonly mode?: string; -} - export enum EditorActivation { /** @@ -169,7 +192,7 @@ export interface IEditorOptions { * Will also not activate the group the editor opens in unless the group is already * the active one. This behaviour can be overridden via the `activation` option. */ - readonly preserveFocus?: boolean; + preserveFocus?: boolean; /** * This option is only relevant if an editor is opened into a group that is not active @@ -179,14 +202,14 @@ export interface IEditorOptions { * By default, the editor group will become active unless `preserveFocus` or `inactive` * is specified. */ - readonly activation?: EditorActivation; + activation?: EditorActivation; /** * Tells the editor to reload the editor input in the editor even if it is identical to the one * already showing. By default, the editor will not reload the input if it is identical to the * one showing. */ - readonly forceReload?: boolean; + forceReload?: boolean; /** * Will reveal the editor if it is already opened and visible in any of the opened editor groups. @@ -194,7 +217,7 @@ export interface IEditorOptions { * Note that this option is just a hint that might be ignored if the user wants to open an editor explicitly * to the side of another one or into a specific editor group. */ - readonly revealIfVisible?: boolean; + revealIfVisible?: boolean; /** * Will reveal the editor if it is already opened (even when not visible) in any of the opened editor groups. @@ -202,24 +225,24 @@ export interface IEditorOptions { * Note that this option is just a hint that might be ignored if the user wants to open an editor explicitly * to the side of another one or into a specific editor group. */ - readonly revealIfOpened?: boolean; + revealIfOpened?: boolean; /** * An editor that is pinned remains in the editor stack even when another editor is being opened. * An editor that is not pinned will always get replaced by another editor that is not pinned. */ - readonly pinned?: boolean; + pinned?: boolean; /** * An editor that is sticky moves to the beginning of the editors list within the group and will remain * there unless explicitly closed. Operations such as "Close All" will not close sticky editors. */ - readonly sticky?: boolean; + sticky?: boolean; /** * The index in the document stack where to insert the editor into when opening. */ - readonly index?: number; + index?: number; /** * An active editor that is opened will show its contents directly. Set to true to open an editor @@ -228,13 +251,13 @@ export interface IEditorOptions { * Will also not activate the group the editor opens in unless the group is already * the active one. This behaviour can be overridden via the `activation` option. */ - readonly inactive?: boolean; + inactive?: boolean; /** * Will not show an error in case opening the editor fails and thus allows to show a custom error * message as needed. By default, an error will be presented as notification if opening was not possible. */ - readonly ignoreError?: boolean; + ignoreError?: boolean; /** * Allows to override the editor that should be used to display the input: @@ -242,7 +265,7 @@ export interface IEditorOptions { * - `string`: specific override by id * - `EditorOverride`: specific override handling */ - readonly override?: string | EditorOverride; + override?: string | EditorOverride; /** * A optional hint to signal in which context the editor opens. @@ -254,7 +277,7 @@ export interface IEditorOptions { * some background task, the notification would show in the background, * not as a modal dialog. */ - readonly context?: EditorOpenContext; + context?: EditorOpenContext; } export interface ITextEditorSelection { @@ -269,14 +292,17 @@ export const enum TextEditorSelectionRevealType { * Option to scroll vertically or horizontally as necessary and reveal a range centered vertically. */ Center = 0, + /** * Option to scroll vertically or horizontally as necessary and reveal a range centered vertically only if it lies outside the viewport. */ CenterIfOutsideViewport = 1, + /** * Option to scroll vertically or horizontally as necessary and reveal a range close to the top of the viewport, but not quite at the top. */ NearTop = 2, + /** * Option to scroll vertically or horizontally as necessary and reveal a range close to the top of the viewport, but not quite at the top. * Only if it lies outside the viewport @@ -289,16 +315,16 @@ export interface ITextEditorOptions extends IEditorOptions { /** * Text editor selection. */ - readonly selection?: ITextEditorSelection; + selection?: ITextEditorSelection; /** * Text editor view state. */ - readonly viewState?: object; + viewState?: object; /** * Option to control the text editor selection reveal type. * Defaults to TextEditorSelectionRevealType.Center */ - readonly selectionRevealType?: TextEditorSelectionRevealType; + selectionRevealType?: TextEditorSelectionRevealType; } diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index f762e9d442..de8fc6efbe 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -65,6 +65,7 @@ export interface NativeParsedArgs { 'install-source'?: string; 'disable-updates'?: boolean; 'disable-keytar'?: boolean; + 'disable-workspace-trust'?: boolean; 'disable-crash-reporter'?: boolean; 'crash-reporter-directory'?: string; 'crash-reporter-id'?: string; diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 09e5ea6932..6892c580bc 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -66,6 +66,9 @@ export interface IEnvironmentService { extensionDevelopmentKind?: ExtensionKind[]; extensionTestsLocationURI?: URI; + // --- workspace trust + disableWorkspaceTrust: boolean; + // --- logging logsPath: string; logLevel?: string; diff --git a/src/vs/platform/environment/common/environmentService.ts b/src/vs/platform/environment/common/environmentService.ts index c04db95f26..35671d6486 100644 --- a/src/vs/platform/environment/common/environmentService.ts +++ b/src/vs/platform/environment/common/environmentService.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IProductService } from 'vs/platform/product/common/productService'; @@ -231,6 +231,9 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron get telemetryLogResource(): URI { return URI.file(join(this.logsPath, 'telemetry.log')); } get disableTelemetry(): boolean { return !!this.args['disable-telemetry']; } + @memoize + get disableWorkspaceTrust(): boolean { return !!this.args['disable-workspace-trust']; } + get args(): NativeParsedArgs { return this._args; } constructor( diff --git a/src/vs/platform/environment/electron-main/environmentMainService.ts b/src/vs/platform/environment/electron-main/environmentMainService.ts index 76a9cdcd13..9c7fc4017e 100644 --- a/src/vs/platform/environment/electron-main/environmentMainService.ts +++ b/src/vs/platform/environment/electron-main/environmentMainService.ts @@ -25,11 +25,8 @@ export interface IEnvironmentMainService extends INativeEnvironmentService { backupHome: string; backupWorkspacesPath: string; - // --- V8 script cache path (ours) - nodeCachedDataDir?: string; - - // --- V8 script cache path (chrome) - chromeCachedDataDir: string; + // --- V8 code cache path + codeCachePath?: string; // --- IPC mainIPCHandle: string; @@ -68,8 +65,5 @@ export class EnvironmentMainService extends NativeEnvironmentService implements get disableKeytar(): boolean { return !!this.args['disable-keytar']; } @memoize - get nodeCachedDataDir(): string | undefined { return process.env['VSCODE_NODE_CACHED_DATA_DIR'] || undefined; } - - @memoize - get chromeCachedDataDir(): string { return join(this.userDataPath, 'Code Cache'); } + get codeCachePath(): string | undefined { return process.env['VSCODE_CODE_CACHE_PATH'] || undefined; } } diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index d0f2e8a440..103c03ef2b 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -54,7 +54,7 @@ export const OPTIONS: OptionDescriptions> = { '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-extensions.") }, - 'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extensions.") }, + 'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extensions."), args: 'category' }, '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.") }, @@ -63,18 +63,18 @@ export const OPTIONS: OptionDescriptions> = { 'verbose': { type: 'boolean', cat: 't', description: localize('verbose', "Print verbose output (implies --wait).") }, 'log': { type: 'string', cat: 't', args: 'level', description: localize('log', "Log level to use. Default is 'info'. Allowed values are 'critical', 'error', 'warn', 'info', 'debug', 'trace', 'off'.") }, 'status': { type: 'boolean', alias: 's', cat: 't', description: localize('status', "Print process usage and diagnostics information.") }, - 'prof-startup': { type: 'boolean', cat: 't', description: localize('prof-startup', "Run CPU profiler during startup") }, + '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'] }, + 'sync': { type: 'string', cat: 't', description: localize('turn sync', "Turn sync on or off."), args: ['on', 'off'] }, 'inspect-extensions': { type: 'string', deprecates: 'debugPluginHost', args: 'port', cat: 't', description: localize('inspect-extensions', "Allow debugging and profiling of extensions. Check the developer tools for the connection URI.") }, 'inspect-brk-extensions': { type: 'string', deprecates: 'debugBrkPluginHost', args: 'port', cat: 't', description: localize('inspect-brk-extensions', "Allow debugging and profiling of extensions with the extension host being paused after start. Check the developer tools for the connection URI.") }, 'disable-gpu': { type: 'boolean', cat: 't', description: localize('disableGPU', "Disable GPU hardware acceleration.") }, - 'max-memory': { type: 'string', cat: 't', description: localize('maxMemory', "Max memory size for a window (in Mbytes).") }, + 'max-memory': { type: 'string', cat: 't', description: localize('maxMemory', "Max memory size for a window (in Mbytes)."), args: 'memory' }, 'telemetry': { type: 'boolean', cat: 't', description: localize('telemetry', "Shows all telemetry events which VS code collects.") }, 'remote': { type: 'string' }, @@ -97,6 +97,7 @@ export const OPTIONS: OptionDescriptions> = { 'disable-telemetry': { type: 'boolean' }, 'disable-updates': { type: 'boolean' }, 'disable-keytar': { type: 'boolean' }, + 'disable-workspace-trust': { type: 'boolean' }, 'disable-crash-reporter': { type: 'boolean' }, 'crash-reporter-directory': { type: 'string' }, 'crash-reporter-id': { type: 'string' }, diff --git a/src/vs/platform/environment/node/userDataPath.js b/src/vs/platform/environment/node/userDataPath.js index 6e7a70f4bd..43558189dc 100644 --- a/src/vs/platform/environment/node/userDataPath.js +++ b/src/vs/platform/environment/node/userDataPath.js @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ /// diff --git a/src/vs/platform/environment/test/node/userDataPath.test.ts b/src/vs/platform/environment/test/node/userDataPath.test.ts index e0eb596af9..54eb343423 100644 --- a/src/vs/platform/environment/test/node/userDataPath.test.ts +++ b/src/vs/platform/environment/test/node/userDataPath.test.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index dc2c1fed55..1190a2eaa5 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -9,7 +9,7 @@ import { getGalleryExtensionId, getGalleryExtensionTelemetryData, adoptToGallery import { getOrDefault } from 'vs/base/common/objects'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IPager } from 'vs/base/common/paging'; -import { IRequestService, asJson, asText } from 'vs/platform/request/common/request'; +import { IRequestService, asJson, asText, isSuccess } from 'vs/platform/request/common/request'; import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request'; import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -26,48 +26,48 @@ import { optional } from 'vs/platform/instantiation/common/instantiation'; import { joinPath } from 'vs/base/common/resources'; interface IRawGalleryExtensionFile { - assetType: string; - source: string; + readonly assetType: string; + readonly source: string; } interface IRawGalleryExtensionProperty { - key: string; - value: string; + readonly key: string; + readonly value: string; } interface IRawGalleryExtensionVersion { - version: string; - lastUpdated: string; - assetUri: string; - fallbackAssetUri: string; - files: IRawGalleryExtensionFile[]; - properties?: IRawGalleryExtensionProperty[]; + readonly version: string; + readonly lastUpdated: string; + readonly assetUri: string; + readonly fallbackAssetUri: string; + readonly files: IRawGalleryExtensionFile[]; + readonly properties?: IRawGalleryExtensionProperty[]; } interface IRawGalleryExtensionStatistics { - statisticName: string; - value: number; + readonly statisticName: string; + readonly value: number; } interface IRawGalleryExtension { - extensionId: string; - extensionName: string; - displayName: string; - shortDescription: string; - publisher: { displayName: string, publisherId: string, publisherName: string; }; - versions: IRawGalleryExtensionVersion[]; - statistics: IRawGalleryExtensionStatistics[]; - flags: string; + readonly extensionId: string; + readonly extensionName: string; + readonly displayName: string; + readonly shortDescription: string; + readonly publisher: { displayName: string, publisherId: string, publisherName: string; }; + readonly versions: IRawGalleryExtensionVersion[]; + readonly statistics: IRawGalleryExtensionStatistics[]; + readonly flags: string; } interface IRawGalleryQueryResult { - results: { - extensions: IRawGalleryExtension[]; - resultMetadata: { - metadataType: string; - metadataItems: { - name: string; - count: number; + readonly results: { + readonly extensions: IRawGalleryExtension[]; + readonly resultMetadata: { + readonly metadataType: string; + readonly metadataItems: { + readonly name: string; + readonly count: number; }[]; }[] }[]; @@ -126,20 +126,20 @@ const PropertyType = { }; interface ICriterium { - filterType: FilterType; - value?: string; + readonly filterType: FilterType; + readonly value?: string; } const DefaultPageSize = 10; interface IQueryState { - pageNumber: number; - pageSize: number; - sortBy: SortBy; - sortOrder: SortOrder; - flags: Flags; - criteria: ICriterium[]; - assetTypes: string[]; + readonly pageNumber: number; + readonly pageSize: number; + readonly sortBy: SortBy; + readonly sortOrder: SortOrder; + readonly flags: Flags; + readonly criteria: ICriterium[]; + readonly assetTypes: string[]; } const DefaultQueryState: IQueryState = { @@ -152,10 +152,33 @@ const DefaultQueryState: IQueryState = { assetTypes: [] }; +type GalleryServiceQueryClassification = { + readonly filterTypes: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + readonly sortBy: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + readonly sortOrder: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + readonly duration: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', 'isMeasurement': true }; + readonly success: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + readonly requestBodySize: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + readonly responseBodySize?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + readonly statusCode?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + readonly errorCode?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + readonly count?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; +}; + type QueryTelemetryData = { - filterTypes: string[]; - sortBy: string; - sortOrder: string; + readonly filterTypes: string[]; + readonly sortBy: string; + readonly sortOrder: string; +}; + +type GalleryServiceQueryEvent = QueryTelemetryData & { + readonly duration: number; + readonly success: boolean; + readonly requestBodySize: string; + readonly responseBodySize?: string; + readonly statusCode?: string; + readonly errorCode?: string; + readonly count?: string; }; class Query { @@ -239,7 +262,7 @@ function getCoreTranslationAssets(version: IRawGalleryExtensionVersion): [string function getRepositoryAsset(version: IRawGalleryExtensionVersion): IGalleryExtensionAsset | null { if (version.properties) { const results = version.properties.filter(p => p.key === AssetType.Repository); - const gitRegExp = new RegExp('((git|ssh|http(s)?)|(git@[\w.]+))(:(//)?)([\w.@\:/\-~]+)(.git)(/)?'); + const gitRegExp = new RegExp('((git|ssh|http(s)?)|(git@[\\w.]+))(:(//)?)([\\w.@\:/\\-~]+)(.git)(/)?'); const uri = results.filter(r => gitRegExp.test(r.value))[0]; return uri ? { uri: uri.value, fallbackUri: uri.value } : null; @@ -380,13 +403,11 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller /* __GDPR__FRAGMENT__ "GalleryExtensionTelemetryData2" : { "index" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "searchText": { "classification": "CustomerContent", "purpose": "FeatureInsight" }, "querySource": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ telemetryData: { index: ((query.pageNumber - 1) * query.pageSize) + index, - searchText: query.searchText, querySource }, preview: getIsPreview(galleryExtension.flags) @@ -489,7 +510,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { const versionAsset = rawExtension.versions.filter(v => v.version === version)[0]; if (versionAsset) { const extension = toExtension(rawExtension, versionAsset, 0, query); - if (extension.properties.engine && isEngineValid(extension.properties.engine, this.productService.version)) { + if (extension.properties.engine && isEngineValid(extension.properties.engine, this.productService.version, this.productService.date)) { return extension; } } @@ -513,20 +534,9 @@ export class ExtensionGalleryService implements IExtensionGalleryService { throw new Error('No extension gallery service configured.'); } - const type = options.names ? 'ids' : (options.text ? 'text' : 'all'); let text = options.text || ''; const pageSize = getOrDefault(options, o => o.pageSize, 50); - type GalleryServiceQueryClassification = { - type: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - text: { classification: 'CustomerContent', purpose: 'FeatureInsight' }; - }; - type GalleryServiceQueryEvent = { - type: string; - text: string; - }; - this.telemetryService.publicLog2('galleryService:query', { type, text }); - let query = new Query() .withFlags(Flags.IncludeLatestVersionOnly, Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeFiles, Flags.IncludeVersionProperties) .withPage(1, pageSize) @@ -676,6 +686,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { extension.extensionId && extension.extensionId.toLocaleLowerCase().indexOf(text) > -1); } + // {{SQL CARBON EDIT}} public static compareByField(a: any, b: any, fieldName: string): number { if (a && !b) { return 1; @@ -702,6 +713,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { if (!this.isEnabled()) { throw new Error('No extension gallery service configured.'); } + // Always exclude non validated and unpublished extensions query = query .withFlags(query.flags, Flags.ExcludeNonValidated) @@ -717,34 +729,56 @@ export class ExtensionGalleryService implements IExtensionGalleryService { 'Content-Length': String(data.length) }; - const context = await this.requestService.request({ - // {{SQL CARBON EDIT}} - type: 'GET', - url: this.api('/extensionquery'), - data, - headers - }, token); + const startTime = new Date().getTime(); + let context: IRequestContext | undefined, error: any, total: number = 0; - // {{SQL CARBON EDIT}} - let extensionPolicy: string = this.configurationService.getValue(ExtensionsPolicyKey); - if (context.res.statusCode && context.res.statusCode >= 400 && context.res.statusCode < 500 || extensionPolicy === ExtensionsPolicy.allowNone) { - return { galleryExtensions: [], total: 0 }; - } - - const result = await asJson(context); - if (result) { - const r = result.results[0]; - const galleryExtensions = r.extensions; - // const resultCount = r.resultMetadata && r.resultMetadata.filter(m => m.metadataType === 'ResultCount')[0]; {{SQL CARBON EDIT}} comment out for no unused - // const total = resultCount && resultCount.metadataItems.filter(i => i.name === 'TotalCount')[0].count || 0; {{SQL CARBON EDIT}} comment out for no unused + try { + context = await this.requestService.request({ + // {{SQL CARBON EDIT}} + type: 'GET', + url: this.api('/extensionquery'), + data, + headers + }, token); // {{SQL CARBON EDIT}} - let filteredExtensionsResult = this.createQueryResult(query, galleryExtensions); + let extensionPolicy: string = this.configurationService.getValue(ExtensionsPolicyKey); + if (context.res.statusCode && context.res.statusCode >= 400 && context.res.statusCode < 500 || extensionPolicy === ExtensionsPolicy.allowNone) { + return { galleryExtensions: [], total: 0 }; + } - return { galleryExtensions: filteredExtensionsResult.galleryExtensions, total: filteredExtensionsResult.total }; - // {{SQL CARBON EDIT}} - End + const result = await asJson(context); + if (result) { + const r = result.results[0]; + const galleryExtensions = r.extensions; + // const resultCount = r.resultMetadata && r.resultMetadata.filter(m => m.metadataType === 'ResultCount')[0]; {{SQL CARBON EDIT}} comment out for no unused + // const total = resultCount && resultCount.metadataItems.filter(i => i.name === 'TotalCount')[0].count || 0; {{SQL CARBON EDIT}} comment out for no unused + + // {{SQL CARBON EDIT}} + let filteredExtensionsResult = this.createQueryResult(query, galleryExtensions); + + return { galleryExtensions: filteredExtensionsResult.galleryExtensions, total: filteredExtensionsResult.total }; + // {{SQL CARBON EDIT}} - End + } + return { galleryExtensions: [], total }; + + } catch (e) { + error = e; + throw e; + } finally { + this.telemetryService.publicLog2('galleryService:query', { + ...query.telemetryData, + requestBodySize: String(data.length), + duration: new Date().getTime() - startTime, + success: !!context && isSuccess(context), + responseBodySize: context?.res.headers['Content-Length'], + statusCode: context ? String(context.res.statusCode) : undefined, + errorCode: error + ? isPromiseCanceledError(error) ? 'canceled' : getErrorMessage(error).startsWith('XHR timeout') ? 'timeout' : 'failed' + : undefined, + count: String(total) + }); } - return { galleryExtensions: [], total: 0 }; } async reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise { @@ -848,7 +882,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { try { engine = await this.getEngine(v); } catch (error) { /* Ignore error and skip version */ } - if (engine && isEngineValid(engine, this.productService.version)) { + if (engine && isEngineValid(engine, this.productService.version, this.productService.date)) { result.push({ version: v!.version, date: v!.lastUpdated }); } })); @@ -914,8 +948,8 @@ export class ExtensionGalleryService implements IExtensionGalleryService { if (!vsCodeEngine && !azDataEngine) { return null; } - const vsCodeEngineValid = !vsCodeEngine || (vsCodeEngine && isEngineValid(vsCodeEngine, this.productService.vscodeVersion)); - const azDataEngineValid = !azDataEngine || (azDataEngine && isEngineValid(azDataEngine, this.productService.version)); + const vsCodeEngineValid = !vsCodeEngine || (vsCodeEngine && isEngineValid(vsCodeEngine, this.productService.vscodeVersion, this.productService.date)); + const azDataEngineValid = !azDataEngine || (azDataEngine && isEngineValid(azDataEngine, this.productService.version, this.productService.date)); if (vsCodeEngineValid && azDataEngineValid) { return version; } @@ -951,13 +985,14 @@ export class ExtensionGalleryService implements IExtensionGalleryService { const version = versions[0]; const engine = await this.getEngine(version); - if (!isEngineValid(engine, this.productService.version)) { + if (!isEngineValid(engine, this.productService.version, this.productService.date)) { return this.getLastValidExtensionVersionRecursively(extension, versions.slice(1)); } - version.properties = version.properties || []; - version.properties.push({ key: PropertyType.Engine, value: engine }); - return version; + return { + ...version, + properties: [...(version.properties || []), { key: PropertyType.Engine, value: engine }] + }; } async getExtensionsReport(): Promise { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 25186849a0..201137aaa1 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -207,6 +207,7 @@ export class ExtensionManagementError extends Error { } export type InstallOptions = { isBuiltin?: boolean, isMachineScoped?: boolean, donotIncludePackAndDependencies?: boolean }; +export type InstallVSIXOptions = InstallOptions & { installOnlyNewlyAddedFromExtensionPack?: boolean }; export type UninstallOptions = { donotIncludePack?: boolean, donotCheckDependents?: boolean }; export const IExtensionManagementService = createDecorator('extensionManagementService'); @@ -221,7 +222,7 @@ export interface IExtensionManagementService { zip(extension: ILocalExtension): Promise; unzip(zipLocation: URI): Promise; getManifest(vsix: URI): Promise; - install(vsix: URI, options?: InstallOptions): Promise; + install(vsix: URI, options?: InstallVSIXOptions): Promise; canInstall(extension: IGalleryExtension): Promise; installFromGallery(extension: IGalleryExtension, options?: InstallOptions): Promise; uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index 127f72acdb..025b54cea0 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, IGalleryExtension, DidUninstallExtensionEvent, IExtensionIdentifier, IGalleryMetadata, IReportedExtension, IExtensionTipsService, InstallOptions, UninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, IGalleryExtension, DidUninstallExtensionEvent, IExtensionIdentifier, IGalleryMetadata, IReportedExtension, IExtensionTipsService, InstallOptions, UninstallOptions, InstallVSIXOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { Emitter, Event } from 'vs/base/common/event'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IURITransformer, DefaultURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc'; @@ -62,7 +62,7 @@ export class ExtensionManagementChannel implements IServerChannel { switch (command) { case 'zip': return this.service.zip(transformIncomingExtension(args[0], uriTransformer)).then(uri => transformOutgoingURI(uri, uriTransformer)); case 'unzip': return this.service.unzip(transformIncomingURI(args[0], uriTransformer)); - case 'install': return this.service.install(transformIncomingURI(args[0], uriTransformer)); + case 'install': return this.service.install(transformIncomingURI(args[0], uriTransformer), args[1]); case 'getManifest': return this.service.getManifest(transformIncomingURI(args[0], uriTransformer)); case 'canInstall': return this.service.canInstall(args[0]); case 'installFromGallery': return this.service.installFromGallery(args[0], args[1]); @@ -112,8 +112,8 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt return Promise.resolve(this.channel.call('unzip', [zipLocation])); } - install(vsix: URI): Promise { - return Promise.resolve(this.channel.call('install', [vsix])).then(local => transformIncomingExtension(local, null)); + install(vsix: URI, options?: InstallVSIXOptions): Promise { + return Promise.resolve(this.channel.call('install', [vsix, options])).then(local => transformIncomingExtension(local, null)); } getManifest(vsix: URI): Promise { diff --git a/src/vs/platform/extensionManagement/node/extensionDownloader.ts b/src/vs/platform/extensionManagement/node/extensionDownloader.ts index 5a5b9e9b03..7c1a98f645 100644 --- a/src/vs/platform/extensionManagement/node/extensionDownloader.ts +++ b/src/vs/platform/extensionManagement/node/extensionDownloader.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { promises } from 'fs'; +import { Promises as FSPromises } from 'vs/base/node/pfs'; import { Disposable } from 'vs/base/common/lifecycle'; import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -77,7 +77,7 @@ export class ExtensionsDownloader extends Disposable { private async rename(from: URI, to: URI, retryUntil: number): Promise { try { - await promises.rename(from.fsPath, to.fsPath); + await FSPromises.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...`); diff --git a/src/vs/platform/extensionManagement/node/extensionLifecycle.ts b/src/vs/platform/extensionManagement/node/extensionLifecycle.ts index 933b549a5e..5b4b34ebd0 100644 --- a/src/vs/platform/extensionManagement/node/extensionLifecycle.ts +++ b/src/vs/platform/extensionManagement/node/extensionLifecycle.ts @@ -12,7 +12,7 @@ import { join } from 'vs/base/common/path'; import { Limiter } from 'vs/base/common/async'; import { Event } from 'vs/base/common/event'; import { Schemas } from 'vs/base/common/network'; -import { rimraf } from 'vs/base/node/pfs'; +import { Promises } from 'vs/base/node/pfs'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; export class ExtensionsLifecycle extends Disposable { @@ -34,7 +34,7 @@ export class ExtensionsLifecycle extends Disposable { this.runLifecycleHook(script.script, 'uninstall', script.args, true, extension) .then(() => this.logService.info(extension.identifier.id, extension.manifest.version, `Finished running post uninstall script`), err => this.logService.error(extension.identifier.id, extension.manifest.version, `Failed to run post uninstall script: ${err}`))); } - return rimraf(this.getExtensionStoragePath(extension)).then(undefined, e => this.logService.error('Error while removing extension storage path', e)); + return Promises.rm(this.getExtensionStoragePath(extension)).then(undefined, e => this.logService.error('Error while removing extension storage path', e)); } private parseScript(extension: ILocalExtension, type: string): { script: string, args: string[] } | null { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 398383debb..25a218b8ee 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -3,11 +3,11 @@ * 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'; import { toDisposable, Disposable } from 'vs/base/common/lifecycle'; +// import { isNonEmptyArray } from 'vs/base/common/arrays'; {{SQL CARBON EDIT}} import { zip, IFile } from 'vs/base/node/zip'; import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, @@ -21,7 +21,8 @@ import { INSTALL_ERROR_INCOMPATIBLE, ExtensionManagementError, InstallOptions, - UninstallOptions + UninstallOptions, + InstallVSIXOptions } 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'; @@ -137,9 +138,9 @@ export class ExtensionManagementService extends Disposable implements IExtension private async collectFiles(extension: ILocalExtension): Promise { const collectFilesFromDirectory = async (dir: string): Promise => { - let entries = await pfs.readdir(dir); + let entries = await pfs.Promises.readdir(dir); entries = entries.map(e => path.join(dir, e)); - const stats = await Promise.all(entries.map(e => fs.promises.stat(e))); + const stats = await Promise.all(entries.map(e => pfs.Promises.stat(e))); let promise: Promise = Promise.resolve([]); stats.forEach((stat, index) => { const entry = entries[index]; @@ -159,7 +160,7 @@ export class ExtensionManagementService extends Disposable implements IExtension return files.map(f => ({ path: `extension/${path.relative(extension.location.fsPath, f)}`, localPath: f })); } - async install(vsix: URI, options: InstallOptions = {}): Promise { + async install(vsix: URI, options: InstallVSIXOptions = {}): Promise { // {{SQL CARBON EDIT}} let startTime = new Date().getTime(); this.logService.trace('ExtensionManagementService#install', vsix.toString()); @@ -172,10 +173,10 @@ export class ExtensionManagementService extends Disposable implements IExtension const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) }; // let operation: InstallOperation = InstallOperation.Install; {{SQL CARBON EDIT}} // {{SQL CARBON EDIT}} - if (manifest.engines?.vscode && !isEngineValid(manifest.engines.vscode, product.vscodeVersion)) { + if (manifest.engines?.vscode && !isEngineValid(manifest.engines.vscode, product.vscodeVersion, product.date)) { throw new Error(nls.localize('incompatible', "Unable to install extension '{0}' as it is not compatible with the current VS Code engine version '{1}'.", identifier.id, product.vscodeVersion)); } - if (manifest.engines?.azdata && !isEngineValid(manifest.engines.azdata, product.version)) { + if (manifest.engines?.azdata && !isEngineValid(manifest.engines.azdata, product.version, product.date)) { throw new Error(nls.localize('incompatibleAzdata', "Unable to install extension '{0}' as it is not compatible with Azure Data Studio '{1}'.", identifier.id, product.version)); } @@ -228,8 +229,9 @@ export class ExtensionManagementService extends Disposable implements IExtension // try { // metadata = await this.getGalleryMetadata(getGalleryExtensionId(manifest.publisher, manifest.name)); // } catch (e) { /* Ignore */ } + // try { - // const local = await this.installFromZipPath(identifierWithVersion, zipPath, isMachineScoped ? { ...(metadata || {}), isMachineScoped } : metadata, operation, token); + // const local = await this.installFromZipPath(identifierWithVersion, zipPath, options.installOnlyNewlyAddedFromExtensionPack ? existing : undefined, { ...(metadata || {}), ...options }, options, operation, token); // this.logService.info('Successfully installed the extension:', identifier.id); // return local; // } catch (e) { @@ -253,11 +255,13 @@ export class ExtensionManagementService extends Disposable implements IExtension } // {{SQL CARBON EDIT}} - /*private async installFromZipPath(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, metadata: IMetadata | undefined, operation: InstallOperation, token: CancellationToken): Promise { + /*private async installFromZipPath(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, existing: ILocalExtension | undefined, metadata: IMetadata | undefined, options: InstallOptions, operation: InstallOperation, token: CancellationToken): Promise { try { const local = await this.installExtension({ zipPath, identifierWithVersion, metadata }, token); try { - await this.installDependenciesAndPackExtensions(local, undefined, options); + if (!options.donotIncludePackAndDependencies) { + await this.installDependenciesAndPackExtensions(local, existing, options); + } } catch (error) { if (isNonEmptyArray(local.manifest.extensionDependencies)) { this.logService.warn(`Cannot install dependencies of extension:`, local.identifier.id, error.message); @@ -657,7 +661,7 @@ export class ExtensionManagementService extends Disposable implements IExtension } private async preUninstallExtension(extension: ILocalExtension): Promise { - const exists = await pfs.exists(extension.location.fsPath); + const exists = await pfs.Promises.exists(extension.location.fsPath); if (!exists) { throw new Error(nls.localize('notExists', "Could not find extension")); } diff --git a/src/vs/platform/extensionManagement/node/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/node/extensionManagementUtil.ts index ee2e0324b4..0e4b129dea 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementUtil.ts @@ -16,4 +16,4 @@ export function getManifest(vsix: string): Promise { throw new Error(localize('invalidManifest', "VSIX invalid: package.json is not a JSON file.")); } }); -} \ No newline at end of file +} diff --git a/src/vs/platform/extensionManagement/node/extensionsManifestCache.ts b/src/vs/platform/extensionManagement/node/extensionsManifestCache.ts index b272745b99..74595df901 100644 --- a/src/vs/platform/extensionManagement/node/extensionsManifestCache.ts +++ b/src/vs/platform/extensionManagement/node/extensionsManifestCache.ts @@ -36,6 +36,6 @@ export class ExtensionsManifestCache extends Disposable { } invalidate(): void { - pfs.rimraf(this.extensionsManifestCache, pfs.RimRafMode.MOVE).then(() => { }, () => { }); + pfs.Promises.rm(this.extensionsManifestCache, pfs.RimRafMode.MOVE).then(() => { }, () => { }); } } diff --git a/src/vs/platform/extensionManagement/node/extensionsScanner.ts b/src/vs/platform/extensionManagement/node/extensionsScanner.ts index 80149ab148..94ae6a738f 100644 --- a/src/vs/platform/extensionManagement/node/extensionsScanner.ts +++ b/src/vs/platform/extensionManagement/node/extensionsScanner.ts @@ -3,7 +3,6 @@ * 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'; @@ -107,10 +106,10 @@ export class ExtensionsScanner extends Disposable { const extensionPath = path.join(this.extensionsPath, folderName); try { - await pfs.rimraf(extensionPath); + await pfs.Promises.rm(extensionPath); } catch (error) { try { - await pfs.rimraf(extensionPath); + await pfs.Promises.rm(extensionPath); } catch (e) { /* ignore */ } throw new ExtensionManagementError(localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionPath, identifierWithVersion.id), INSTALL_ERROR_DELETING); } @@ -127,7 +126,7 @@ export class ExtensionsScanner extends Disposable { this.logService.info('Renamed to', extensionPath); } catch (error) { try { - await pfs.rimraf(tempPath); + await pfs.Promises.rm(tempPath); } catch (e) { /* ignore */ } if (error.code === 'ENOTEMPTY') { this.logService.info(`Rename failed because extension was installed by another source. So ignoring renaming.`, identifierWithVersion.id); @@ -159,10 +158,10 @@ export class ExtensionsScanner extends Disposable { storedMetadata.isBuiltin = storedMetadata.isBuiltin || undefined; storedMetadata.installedTimestamp = storedMetadata.installedTimestamp || undefined; const manifestPath = path.join(local.location.fsPath, 'package.json'); - const raw = await fs.promises.readFile(manifestPath, 'utf8'); + const raw = await pfs.Promises.readFile(manifestPath, 'utf8'); const { manifest } = await this.parseManifest(raw); (manifest as ILocalExtensionManifest).__metadata = storedMetadata; - await pfs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t')); + await pfs.Promises.writeFile(manifestPath, JSON.stringify(manifest, null, '\t')); return local; } @@ -192,7 +191,7 @@ export class ExtensionsScanner extends Disposable { return this.uninstalledFileLimiter.queue(async () => { let raw: string | undefined; try { - raw = await fs.promises.readFile(this.uninstalledPath, 'utf8'); + raw = await pfs.Promises.readFile(this.uninstalledPath, 'utf8'); } catch (err) { if (err.code !== 'ENOENT') { throw err; @@ -209,9 +208,9 @@ export class ExtensionsScanner extends Disposable { if (updateFn) { updateFn(uninstalled); if (Object.keys(uninstalled).length) { - await pfs.writeFile(this.uninstalledPath, JSON.stringify(uninstalled)); + await pfs.Promises.writeFile(this.uninstalledPath, JSON.stringify(uninstalled)); } else { - await pfs.rimraf(this.uninstalledPath); + await pfs.Promises.rm(this.uninstalledPath); } } @@ -221,7 +220,7 @@ export class ExtensionsScanner extends Disposable { async removeExtension(extension: ILocalExtension, type: string): Promise { this.logService.trace(`Deleting ${type} extension from disk`, extension.identifier.id, extension.location.fsPath); - await pfs.rimraf(extension.location.fsPath); + await pfs.Promises.rm(extension.location.fsPath); this.logService.info('Deleted from disk', extension.identifier.id, extension.location.fsPath); } @@ -235,7 +234,7 @@ export class ExtensionsScanner extends Disposable { // Clean the location try { - await pfs.rimraf(location); + await pfs.Promises.rm(location); } catch (e) { throw new ExtensionManagementError(this.joinErrors(e).message, INSTALL_ERROR_DELETING); } @@ -244,14 +243,14 @@ export class ExtensionsScanner extends Disposable { await extract(zipPath, location, { sourcePath: 'extension', overwrite: true }, token); this.logService.info(`Extracted extension to ${location}:`, identifier.id); } catch (e) { - try { await pfs.rimraf(location); } catch (e) { /* Ignore */ } + try { await pfs.Promises.rm(location); } catch (e) { /* Ignore */ } throw new ExtensionManagementError(e.message, e instanceof ExtractError && e.type ? e.type : INSTALL_ERROR_EXTRACTING); } } private async rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise { try { - await fs.promises.rename(extractPath, renamePath); + await pfs.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); @@ -393,9 +392,9 @@ export class ExtensionsScanner extends Disposable { private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: IStoredMetadata | null; }> { const promises = [ - fs.promises.readFile(path.join(extensionPath, 'package.json'), 'utf8') + pfs.Promises.readFile(path.join(extensionPath, 'package.json'), 'utf8') .then(raw => this.parseManifest(raw)), - fs.promises.readFile(path.join(extensionPath, 'package.nls.json'), 'utf8') + pfs.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/node/extensionsWatcher.ts b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts index fb49d2bf0d..1d8b1ced6c 100644 --- a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts +++ b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; diff --git a/src/vs/platform/extensions/common/extensionValidator.ts b/src/vs/platform/extensions/common/extensionValidator.ts index 2bbd518d38..219dd7b3bc 100644 --- a/src/vs/platform/extensions/common/extensionValidator.ts +++ b/src/vs/platform/extensions/common/extensionValidator.ts @@ -24,10 +24,12 @@ export interface INormalizedVersion { minorMustEqual: boolean; patchBase: number; patchMustEqual: boolean; + notBefore: number; /* milliseconds timestamp, or 0 */ isMinimum: boolean; } const VERSION_REGEXP = /^(\^|>=)?((\d+)|x)\.((\d+)|x)\.((\d+)|x)(\-.*)?$/; +const NOT_BEFORE_REGEXP = /^-(\d{4})(\d{2})(\d{2})$/; export function isValidVersionStr(version: string): boolean { version = version.trim(); @@ -93,6 +95,15 @@ export function normalizeVersion(version: IParsedVersion | null): INormalizedVer } } + let notBefore = 0; + if (version.preRelease) { + const match = NOT_BEFORE_REGEXP.exec(version.preRelease); + if (match) { + const [, year, month, day] = match; + notBefore = Date.UTC(Number(year), Number(month) - 1, Number(day)); + } + } + return { majorBase: majorBase, majorMustEqual: majorMustEqual, @@ -100,16 +111,24 @@ export function normalizeVersion(version: IParsedVersion | null): INormalizedVer minorMustEqual: minorMustEqual, patchBase: patchBase, patchMustEqual: patchMustEqual, - isMinimum: version.hasGreaterEquals + isMinimum: version.hasGreaterEquals, + notBefore, }; } -export function isValidVersion(_version: string | INormalizedVersion, _desiredVersion: string | INormalizedVersion): boolean { +export function isValidVersion(_inputVersion: string | INormalizedVersion, _inputDate: ProductDate, _desiredVersion: string | INormalizedVersion): boolean { let version: INormalizedVersion | null; - if (typeof _version === 'string') { - version = normalizeVersion(parseVersion(_version)); + if (typeof _inputVersion === 'string') { + version = normalizeVersion(parseVersion(_inputVersion)); } else { - version = _version; + version = _inputVersion; + } + + let productTs: number | undefined; + if (_inputDate instanceof Date) { + productTs = _inputDate.getTime(); + } else if (typeof _inputDate === 'string') { + productTs = new Date(_inputDate).getTime(); } let desiredVersion: INormalizedVersion | null; @@ -130,6 +149,7 @@ export function isValidVersion(_version: string | INormalizedVersion, _desiredVe let desiredMajorBase = desiredVersion.majorBase; let desiredMinorBase = desiredVersion.minorBase; let desiredPatchBase = desiredVersion.patchBase; + let desiredNotBefore = desiredVersion.notBefore; let majorMustEqual = desiredVersion.majorMustEqual; let minorMustEqual = desiredVersion.minorMustEqual; @@ -152,6 +172,10 @@ export function isValidVersion(_version: string | INormalizedVersion, _desiredVe return false; } + if (productTs && productTs < desiredNotBefore) { + return false; + } + return patchBase >= desiredPatchBase; } @@ -200,6 +224,11 @@ export function isValidVersion(_version: string | INormalizedVersion, _desiredVe } // at this point, patchBase are equal + + if (productTs && productTs < desiredNotBefore) { + return false; + } + return true; } @@ -213,7 +242,9 @@ export interface IReducedExtensionDescription { main?: string; } -export function isValidExtensionVersion(version: string, extensionDesc: IReducedExtensionDescription, notices: string[]): boolean { +type ProductDate = string | Date | undefined; + +export function isValidExtensionVersion(version: string, date: ProductDate, extensionDesc: IReducedExtensionDescription, notices: string[]): boolean { if (extensionDesc.isBuiltin || typeof extensionDesc.main === 'undefined') { // No version check for builtin or declarative extensions @@ -221,16 +252,16 @@ export function isValidExtensionVersion(version: string, extensionDesc: IReduced } // {{SQL CARBON EDIT}} - return extensionDesc.engines.azdata ? extensionDesc.engines.azdata === '*' || isVersionValid(version, extensionDesc.engines.azdata, notices) : true; + return extensionDesc.engines.azdata ? extensionDesc.engines.azdata === '*' || isVersionValid(version, date, extensionDesc.engines.azdata, notices) : true; } // {{SQL CARBON EDIT}} -export function isEngineValid(engine: string, version: string): boolean { +export function isEngineValid(engine: string, version: string, date: ProductDate): boolean { // TODO@joao: discuss with alex '*' doesn't seem to be a valid engine version - return engine === '*' || isVersionValid(version, engine); + return engine === '*' || isVersionValid(version, date, engine); } -export function isVersionValid(currentVersion: string, requestedVersion: string, notices: string[] = []): boolean { +function isVersionValid(currentVersion: string, date: ProductDate, requestedVersion: string, notices: string[] = []): boolean { let desiredVersion = normalizeVersion(parseVersion(requestedVersion)); if (!desiredVersion) { @@ -255,7 +286,7 @@ export function isVersionValid(currentVersion: string, requestedVersion: string, } } - if (!isValidVersion(currentVersion, desiredVersion)) { + if (!isValidVersion(currentVersion, date, desiredVersion)) { notices.push(nls.localize('versionMismatch', "Extension is not compatible with Code {0}. Extension requires: {1}.", currentVersion, requestedVersion)); return false; } diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 4a4f83b4dd..2224d22f29 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -123,10 +123,12 @@ export interface IAuthenticationContribution { export interface IWalkthroughStep { readonly id: string; readonly title: string; - readonly description: string; + readonly description: string | undefined; readonly media: - | { path: string | { dark: string, light: string, hc: string }, altText: string } - | { path: string, }, + | { image: string | { dark: string, light: string, hc: string }, altText: string, markdown?: never } + | { markdown: string, image?: never } + readonly completionEvents?: string[]; + /** @deprecated use `completionEvents: 'onCommand:...'` */ readonly doneOn?: { command: string }; readonly when?: string; } @@ -136,7 +138,6 @@ export interface IWalkthrough { readonly title: string; readonly description: string; readonly steps: IWalkthroughStep[]; - readonly primary?: boolean; readonly when?: string; } @@ -173,13 +174,30 @@ export interface IExtensionContributions { } export interface IExtensionCapabilities { - readonly virtualWorkspaces?: boolean; + readonly virtualWorkspaces?: ExtensionVirtualWorkpaceSupport; readonly untrustedWorkspaces?: ExtensionUntrustedWorkspaceSupport; } + + export type ExtensionKind = 'ui' | 'workspace' | 'web'; -export type ExtensionUntrustedWorkpaceSupportType = boolean | 'limited'; -export type ExtensionUntrustedWorkspaceSupport = { supported: true; } | { supported: false, description: string } | { supported: 'limited', description: string, restrictedConfigurations?: string[] }; + +export type LimitedWorkpaceSupportType = 'limited'; +export type ExtensionUntrustedWorkpaceSupportType = boolean | LimitedWorkpaceSupportType; +export type ExtensionUntrustedWorkspaceSupport = { supported: true; } | { supported: false, description: string } | { supported: LimitedWorkpaceSupportType, description: string, restrictedConfigurations?: string[] }; + +export type ExtensionVirtualWorkpaceSupportType = boolean | LimitedWorkpaceSupportType; +export type ExtensionVirtualWorkpaceSupport = boolean | { supported: true; } | { supported: false | LimitedWorkpaceSupportType, description: string }; + +export function getWorkpaceSupportTypeMessage(supportType: ExtensionUntrustedWorkspaceSupport | ExtensionVirtualWorkpaceSupport | undefined): string | undefined { + if (typeof supportType === 'object' && supportType !== null) { + if (supportType.supported !== true) { + return supportType.description; + } + } + return undefined; +} + export function isIExtensionIdentifier(thing: any): thing is IExtensionIdentifier { return thing diff --git a/src/vs/platform/extensions/test/common/extensionValidator.test.ts b/src/vs/platform/extensions/test/common/extensionValidator.test.ts index 17d02188ff..951f28349f 100644 --- a/src/vs/platform/extensions/test/common/extensionValidator.test.ts +++ b/src/vs/platform/extensions/test/common/extensionValidator.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import { INormalizedVersion, IParsedVersion, IReducedExtensionDescription, isValidExtensionVersion, isValidVersion, isValidVersionStr, normalizeVersion, parseVersion } from 'vs/platform/extensions/common/extensionValidator'; suite('Extension Version Validator', () => { + const productVersion = '2021-05-11T21:54:30.577Z'; test('isValidVersionStr', () => { assert.strictEqual(isValidVersionStr('0.10.0-dev'), true); @@ -53,13 +54,16 @@ suite('Extension Version Validator', () => { }); test('normalizeVersion', () => { - function assertNormalizeVersion(version: string, majorBase: number, majorMustEqual: boolean, minorBase: number, minorMustEqual: boolean, patchBase: number, patchMustEqual: boolean, isMinimum: boolean): void { + function assertNormalizeVersion(version: string, majorBase: number, majorMustEqual: boolean, minorBase: number, minorMustEqual: boolean, patchBase: number, patchMustEqual: boolean, isMinimum: boolean, notBefore = 0): void { const actual = normalizeVersion(parseVersion(version)); - const expected: INormalizedVersion = { majorBase, majorMustEqual, minorBase, minorMustEqual, patchBase, patchMustEqual, isMinimum }; + const expected: INormalizedVersion = { majorBase, majorMustEqual, minorBase, minorMustEqual, patchBase, patchMustEqual, isMinimum, notBefore }; assert.deepStrictEqual(actual, expected, 'parseVersion for ' + version); } - assertNormalizeVersion('0.10.0-dev', 0, true, 10, true, 0, true, false); + assertNormalizeVersion('0.10.0-dev', 0, true, 10, true, 0, true, false, 0); + assertNormalizeVersion('0.10.0-222222222', 0, true, 10, true, 0, true, false, 0); + assertNormalizeVersion('0.10.0-20210511', 0, true, 10, true, 0, true, false, new Date('2021-05-11T00:00:00Z').getTime()); + assertNormalizeVersion('0.10.0', 0, true, 10, true, 0, true, false); assertNormalizeVersion('0.10.1', 0, true, 10, true, 1, true, false); assertNormalizeVersion('0.10.100', 0, true, 10, true, 100, true, false); @@ -75,11 +79,12 @@ suite('Extension Version Validator', () => { assertNormalizeVersion('>=0.0.1', 0, true, 0, true, 1, true, true); assertNormalizeVersion('>=2.4.3', 2, true, 4, true, 3, true, true); + assertNormalizeVersion('>=2.4.3', 2, true, 4, true, 3, true, true); }); test('isValidVersion', () => { function testIsValidVersion(version: string, desiredVersion: string, expectedResult: boolean): void { - let actual = isValidVersion(version, desiredVersion); + let actual = isValidVersion(version, productVersion, desiredVersion); assert.strictEqual(actual, expectedResult, 'extension - vscode: ' + version + ', desiredVersion: ' + desiredVersion + ' should be ' + expectedResult); } @@ -211,7 +216,7 @@ suite('Extension Version Validator', () => { main: hasMain ? 'something' : undefined }; let reasons: string[] = []; - let actual = isValidExtensionVersion(version, desc, reasons); + let actual = isValidExtensionVersion(version, productVersion, desc, reasons); assert.strictEqual(actual, expectedResult, 'version: ' + version + ', desiredVersion: ' + desiredVersion + ', desc: ' + JSON.stringify(desc) + ', reasons: ' + JSON.stringify(reasons)); } @@ -390,5 +395,12 @@ suite('Extension Version Validator', () => { testIsValidVersion('2.0.0', '^1.100.0', false); testIsValidVersion('2.0.0', '^2.0.0', true); testIsValidVersion('2.0.0', '*', false); // fails due to lack of specificity + + // date tags + testIsValidVersion('1.10.0', '^1.10.0-20210511', true); // current date + testIsValidVersion('1.10.0', '^1.10.0-20210510', true); // before date + testIsValidVersion('1.10.0', '^1.10.0-20210512', false); // future date + testIsValidVersion('1.10.1', '^1.10.0-20200101', true); // before date, but ahead version + testIsValidVersion('1.11.0', '^1.10.0-20200101', true); }); }); diff --git a/src/vs/workbench/contrib/externalTerminal/common/externalTerminal.ts b/src/vs/platform/externalTerminal/common/externalTerminal.ts similarity index 67% rename from src/vs/workbench/contrib/externalTerminal/common/externalTerminal.ts rename to src/vs/platform/externalTerminal/common/externalTerminal.ts index f45d1f5d1c..70d702c1ec 100644 --- a/src/vs/workbench/contrib/externalTerminal/common/externalTerminal.ts +++ b/src/vs/platform/externalTerminal/common/externalTerminal.ts @@ -6,7 +6,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ITerminalEnvironment } from 'vs/platform/terminal/common/terminal'; -export const IExternalTerminalService = createDecorator('nativeTerminalService'); +export const IExternalTerminalService = createDecorator('externalTerminal'); export interface IExternalTerminalSettings { linuxExec?: string; @@ -14,10 +14,17 @@ export interface IExternalTerminalSettings { windowsExec?: string; } +export interface ITerminalForPlatform { + windows: string, + linux: string, + osx: string +} + export interface IExternalTerminalService { readonly _serviceBrand: undefined; - openTerminal(path: string): void; + openTerminal(path: string): Promise; runInTerminal(title: string, cwd: string, args: string[], env: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise; + getDefaultTerminalForPlatforms(): Promise; } export interface IExternalTerminalConfiguration { @@ -26,3 +33,11 @@ export interface IExternalTerminalConfiguration { external: IExternalTerminalSettings; }; } + +export const DEFAULT_TERMINAL_OSX = 'Terminal.app'; + +export const IExternalTerminalMainService = createDecorator('externalTerminal'); + +export interface IExternalTerminalMainService extends IExternalTerminalService { + readonly _serviceBrand: undefined; +} diff --git a/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.test.ts b/src/vs/platform/externalTerminal/electron-main/externalTerminalService.test.ts similarity index 95% rename from src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.test.ts rename to src/vs/platform/externalTerminal/electron-main/externalTerminalService.test.ts index f6dfa9e9b7..c3696b4945 100644 --- a/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.test.ts +++ b/src/vs/platform/externalTerminal/electron-main/externalTerminalService.test.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { deepEqual, equal } from 'assert'; -import { WindowsExternalTerminalService, LinuxExternalTerminalService, MacExternalTerminalService } from 'vs/workbench/contrib/externalTerminal/node/externalTerminalService'; -import { DEFAULT_TERMINAL_OSX } from 'vs/workbench/contrib/externalTerminal/node/externalTerminal'; +import { DEFAULT_TERMINAL_OSX } from 'vs/platform/externalTerminal/common/externalTerminal'; +import { WindowsExternalTerminalService, MacExternalTerminalService, LinuxExternalTerminalService } from 'vs/platform/externalTerminal/node/externalTerminalService'; suite('ExternalTerminalService', () => { let mockOnExit: Function; diff --git a/src/vs/platform/externalTerminal/electron-sandbox/externalTerminalMainService.ts b/src/vs/platform/externalTerminal/electron-sandbox/externalTerminalMainService.ts new file mode 100644 index 0000000000..edb62b4e34 --- /dev/null +++ b/src/vs/platform/externalTerminal/electron-sandbox/externalTerminalMainService.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 { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { registerMainProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services'; +import { IExternalTerminalService } from 'vs/platform/externalTerminal/common/externalTerminal'; + +export const IExternalTerminalMainService = createDecorator('externalTerminal'); + +export interface IExternalTerminalMainService extends IExternalTerminalService { + readonly _serviceBrand: undefined; +} + +registerMainProcessRemoteService(IExternalTerminalMainService, 'externalTerminal', { supportsDelayedInstantiation: true }); diff --git a/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts b/src/vs/platform/externalTerminal/node/externalTerminalService.ts similarity index 75% rename from src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts rename to src/vs/platform/externalTerminal/node/externalTerminalService.ts index 9db6cd60fd..776e48831c 100644 --- a/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts +++ b/src/vs/platform/externalTerminal/node/externalTerminalService.ts @@ -9,72 +9,42 @@ import * as processes from 'vs/base/node/processes'; import * as nls from 'vs/nls'; import * as pfs from 'vs/base/node/pfs'; import * as env from 'vs/base/common/platform'; -import { IExternalTerminalService, IExternalTerminalConfiguration, IExternalTerminalSettings } from 'vs/workbench/contrib/externalTerminal/common/externalTerminal'; +import { IExternalTerminalConfiguration, IExternalTerminalSettings, DEFAULT_TERMINAL_OSX, ITerminalForPlatform, IExternalTerminalMainService } from 'vs/platform/externalTerminal/common/externalTerminal'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { optional } from 'vs/platform/instantiation/common/instantiation'; -import { DEFAULT_TERMINAL_OSX } from 'vs/workbench/contrib/externalTerminal/node/externalTerminal'; import { FileAccess } from 'vs/base/common/network'; import { ITerminalEnvironment } from 'vs/platform/terminal/common/terminal'; +import { sanitizeProcessEnvironment } from 'vs/base/common/processes'; const TERMINAL_TITLE = nls.localize('console.title', "VS Code Console"); -export class WindowsExternalTerminalService implements IExternalTerminalService { +abstract class ExternalTerminalService { public _serviceBrand: undefined; - private static readonly CMD = 'cmd.exe'; + async getDefaultTerminalForPlatforms(): Promise { + const linuxTerminal = await LinuxExternalTerminalService.getDefaultTerminalLinuxReady(); + return { windows: WindowsExternalTerminalService.getDefaultTerminalWindows(), linux: linuxTerminal, osx: 'xterm' }; + } +} - private readonly _configurationService?: IConfigurationService; +export class WindowsExternalTerminalService extends ExternalTerminalService implements IExternalTerminalMainService { + private static readonly CMD = 'cmd.exe'; + private static _DEFAULT_TERMINAL_WINDOWS: string; constructor( - @optional(IConfigurationService) configurationService: IConfigurationService + @optional(IConfigurationService) private readonly _configurationService: IConfigurationService ) { - this._configurationService = configurationService; + super(); } - public openTerminal(cwd?: string): void { - if (this._configurationService) { - const configuration = this._configurationService.getValue(); - this.spawnTerminal(cp, configuration, processes.getWindowsShell(), cwd); - } + public openTerminal(cwd?: string): Promise { + const configuration = this._configurationService.getValue(); + return this.spawnTerminal(cp, configuration, processes.getWindowsShell(), cwd); } - public runInTerminal(title: string, dir: string, args: string[], envVars: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise { - - const exec = settings.windowsExec || WindowsExternalTerminalService.getDefaultTerminalWindows(); - - return new Promise((resolve, reject) => { - - const title = `"${dir} - ${TERMINAL_TITLE}"`; - const command = `""${args.join('" "')}" & pause"`; // use '|' to only pause on non-zero exit code - - const cmdArgs = [ - '/c', 'start', title, '/wait', exec, '/c', command - ]; - - // merge environment variables into a copy of the process.env - const env = Object.assign({}, process.env, envVars); - - // delete environment variables that have a null value - Object.keys(env).filter(v => env[v] === null).forEach(key => delete env[key]); - - const options: any = { - cwd: dir, - env: env, - windowsVerbatimArguments: true - }; - - const cmd = cp.spawn(WindowsExternalTerminalService.CMD, cmdArgs, options); - cmd.on('error', err => { - reject(improveError(err)); - }); - - resolve(undefined); - }); - } - - private spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalConfiguration, command: string, cwd?: string): Promise { + public spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalConfiguration, command: string, cwd?: string): Promise { const terminalConfig = configuration.terminal.external; - const exec = terminalConfig.windowsExec || WindowsExternalTerminalService.getDefaultTerminalWindows(); + const exec = terminalConfig?.windowsExec || WindowsExternalTerminalService.getDefaultTerminalWindows(); // Make the drive letter uppercase on Windows (see #9448) if (cwd && cwd[1] === ':') { @@ -102,14 +72,45 @@ export class WindowsExternalTerminalService implements IExternalTerminalService } return new Promise((c, e) => { - const env = cwd ? { cwd: cwd } : undefined; - const child = spawner.spawn(command, cmdArgs, env); + const env = getSanitizedEnvironment(process); + const child = spawner.spawn(command, cmdArgs, { cwd, env }); child.on('error', e); child.on('exit', () => c()); }); } - private static _DEFAULT_TERMINAL_WINDOWS: string; + public runInTerminal(title: string, dir: string, args: string[], envVars: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise { + const exec = 'windowsExec' in settings && settings.windowsExec ? settings.windowsExec : WindowsExternalTerminalService.getDefaultTerminalWindows(); + + return new Promise((resolve, reject) => { + + const title = `"${dir} - ${TERMINAL_TITLE}"`; + const command = `""${args.join('" "')}" & pause"`; // use '|' to only pause on non-zero exit code + + const cmdArgs = [ + '/c', 'start', title, '/wait', exec, '/c', command + ]; + + // merge environment variables into a copy of the process.env + const env = Object.assign({}, getSanitizedEnvironment(process), envVars); + + // delete environment variables that have a null value + Object.keys(env).filter(v => env[v] === null).forEach(key => delete env[key]); + + const options: any = { + cwd: dir, + env: env, + windowsVerbatimArguments: true + }; + + const cmd = cp.spawn(WindowsExternalTerminalService.CMD, cmdArgs, options); + cmd.on('error', err => { + reject(improveError(err)); + }); + + resolve(undefined); + }); + } public static getDefaultTerminalWindows(): string { if (!WindowsExternalTerminalService._DEFAULT_TERMINAL_WINDOWS) { @@ -120,24 +121,18 @@ export class WindowsExternalTerminalService implements IExternalTerminalService } } -export class MacExternalTerminalService implements IExternalTerminalService { - public _serviceBrand: undefined; - +export class MacExternalTerminalService extends ExternalTerminalService implements IExternalTerminalMainService { private static readonly OSASCRIPT = '/usr/bin/osascript'; // osascript is the AppleScript interpreter on OS X - private readonly _configurationService?: IConfigurationService; - constructor( - @optional(IConfigurationService) configurationService: IConfigurationService + @optional(IConfigurationService) private readonly _configurationService: IConfigurationService ) { - this._configurationService = configurationService; + super(); } - public openTerminal(cwd?: string): void { - if (this._configurationService) { - const configuration = this._configurationService.getValue(); - this.spawnTerminal(cp, configuration, cwd); - } + public openTerminal(cwd?: string): Promise { + const configuration = this._configurationService.getValue(); + return this.spawnTerminal(cp, configuration, cwd); } public runInTerminal(title: string, dir: string, args: string[], envVars: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise { @@ -204,9 +199,9 @@ export class MacExternalTerminalService implements IExternalTerminalService { }); } - private spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalConfiguration, cwd?: string): Promise { + spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalConfiguration, cwd?: string): Promise { const terminalConfig = configuration.terminal.external; - const terminalApp = terminalConfig.osxExec || DEFAULT_TERMINAL_OSX; + const terminalApp = terminalConfig?.osxExec || DEFAULT_TERMINAL_OSX; return new Promise((c, e) => { const args = ['-a', terminalApp]; @@ -220,24 +215,19 @@ export class MacExternalTerminalService implements IExternalTerminalService { } } -export class LinuxExternalTerminalService implements IExternalTerminalService { - public _serviceBrand: undefined; +export class LinuxExternalTerminalService extends ExternalTerminalService implements IExternalTerminalMainService { private static readonly WAIT_MESSAGE = nls.localize('press.any.key', "Press any key to continue..."); - private readonly _configurationService?: IConfigurationService; - constructor( - @optional(IConfigurationService) configurationService: IConfigurationService + @optional(IConfigurationService) private readonly _configurationService: IConfigurationService ) { - this._configurationService = configurationService; + super(); } - public openTerminal(cwd?: string): void { - if (this._configurationService) { - const configuration = this._configurationService.getValue(); - this.spawnTerminal(cp, configuration, cwd); - } + public openTerminal(cwd?: string): Promise { + const configuration = this._configurationService.getValue(); + return this.spawnTerminal(cp, configuration, cwd); } public runInTerminal(title: string, dir: string, args: string[], envVars: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise { @@ -296,27 +286,13 @@ export class LinuxExternalTerminalService implements IExternalTerminalService { }); } - private spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalConfiguration, cwd?: string): Promise { - const terminalConfig = configuration.terminal.external; - const execPromise = terminalConfig.linuxExec ? Promise.resolve(terminalConfig.linuxExec) : LinuxExternalTerminalService.getDefaultTerminalLinuxReady(); - - return new Promise((c, e) => { - execPromise.then(exec => { - const env = cwd ? { cwd } : undefined; - const child = spawner.spawn(exec, [], env); - child.on('error', e); - child.on('exit', () => c()); - }); - }); - } - private static _DEFAULT_TERMINAL_LINUX_READY: Promise; public static async getDefaultTerminalLinuxReady(): Promise { if (!LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY) { LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY = new Promise(async r => { if (env.isLinux) { - const isDebian = await pfs.exists('/etc/debian_version'); + const isDebian = await pfs.Promises.exists('/etc/debian_version'); if (isDebian) { r('x-terminal-emulator'); } else if (process.env.DESKTOP_SESSION === 'gnome' || process.env.DESKTOP_SESSION === 'gnome-classic') { @@ -337,6 +313,26 @@ export class LinuxExternalTerminalService implements IExternalTerminalService { } return LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY; } + + spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalConfiguration, cwd?: string): Promise { + const terminalConfig = configuration.terminal.external; + const execPromise = terminalConfig?.linuxExec ? Promise.resolve(terminalConfig.linuxExec) : LinuxExternalTerminalService.getDefaultTerminalLinuxReady(); + + return new Promise((c, e) => { + execPromise.then(exec => { + const env = getSanitizedEnvironment(process); + const child = spawner.spawn(exec, [], { cwd, env }); + child.on('error', e); + child.on('exit', () => c()); + }); + }); + } +} + +function getSanitizedEnvironment(process: NodeJS.Process) { + const env = process.env; + sanitizeProcessEnvironment(env); + return env; } /** diff --git a/src/vs/platform/files/browser/htmlFileSystemProvider.ts b/src/vs/platform/files/browser/htmlFileSystemProvider.ts index 435ec9ac0c..50f366497c 100644 --- a/src/vs/platform/files/browser/htmlFileSystemProvider.ts +++ b/src/vs/platform/files/browser/htmlFileSystemProvider.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts b/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts index 30f6ed8aee..08992e979e 100644 --- a/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts +++ b/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts @@ -23,7 +23,7 @@ const ERR_FILE_NOT_DIR = createFileSystemProviderError(localize('fileNotDirector 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); +const ERR_UNKNOWN_INTERNAL = (message: string) => createFileSystemProviderError(localize('internal', "Internal error occurred in IndexedDB File System Provider. ({0})", message), FileSystemProviderErrorCode.Unknown); export class IndexedDB { @@ -99,7 +99,7 @@ class IndexedDBFileSystemNode { } - read(path: string) { + read(path: string): IndexedDBFileSystemEntry | undefined { return this.doRead(path.split('/').filter(p => p.length)); } @@ -283,10 +283,7 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy async readFile(resource: URI): Promise { 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 = () => { + transaction.oncomplete = () => { if (request.result instanceof Uint8Array) { c(request.result); } else if (typeof request.result === 'string') { @@ -300,6 +297,10 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy } } }; + transaction.onerror = () => e(transaction.error); + + const objectStore = transaction.objectStore(this.store); + const request = objectStore.get(resource.path); }); (await this.getFiletree()).add(resource.path, { type: 'file', size: buffer.byteLength }); @@ -374,10 +375,7 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy 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 = () => { + transaction.oncomplete = () => { const rootNode = new IndexedDBFileSystemNode({ children: new Map(), path: '', @@ -387,6 +385,10 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy keys.forEach(key => rootNode.add(key, { type: 'file' })); c(rootNode); }; + transaction.onerror = () => e(transaction.error); + + const objectStore = transaction.objectStore(this.store); + const request = objectStore.getAllKeys(); }); } return this.cachedFiletree; @@ -397,41 +399,44 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy return new Promise((c, e) => { const fileBatch = this.fileWriteBatch; this.fileWriteBatch = []; - if (fileBatch.length === 0) { return c(); } + if (fileBatch.length === 0) { + return c(); + } const transaction = this.database.transaction([this.store], 'readwrite'); + transaction.oncomplete = () => c(); transaction.onerror = () => e(transaction.error); const objectStore = transaction.objectStore(this.store); - let request: IDBRequest = undefined!; for (const entry of fileBatch) { - request = objectStore.put(entry.content, entry.resource.path); + objectStore.put(entry.content, entry.resource.path); } - request.onsuccess = () => c(); }); } 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); - let request: IDBRequest = undefined!; - for (const key of keys) { - request = objectStore.delete(key); + if (keys.length === 0) { + return c(); } - request.onsuccess = () => c(); + const transaction = this.database.transaction([this.store], 'readwrite'); + transaction.oncomplete = () => c(); + transaction.onerror = () => e(transaction.error); + const objectStore = transaction.objectStore(this.store); + for (const key of keys) { + objectStore.delete(key); + } }); } reset(): Promise { return new Promise(async (c, e) => { const transaction = this.database.transaction([this.store], 'readwrite'); + transaction.oncomplete = () => c(); + transaction.onerror = () => e(transaction.error); + const objectStore = transaction.objectStore(this.store); - const request = objectStore.clear(); - request.onerror = () => e(request.error); - request.onsuccess = () => c(); + objectStore.clear(); }); } } diff --git a/src/vs/platform/files/common/fileService.ts b/src/vs/platform/files/common/fileService.ts index 352f55523d..eb05841eb3 100644 --- a/src/vs/platform/files/common/fileService.ts +++ b/src/vs/platform/files/common/fileService.ts @@ -6,7 +6,7 @@ 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, IReadFileStreamOptions, FileDeleteOptions } from 'vs/platform/files/common/files'; +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, IReadFileStreamOptions, FileDeleteOptions, FilePermission, NotModifiedSinceFileOperationError } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Emitter } from 'vs/base/common/event'; import { IExtUri, extUri, extUriIgnorePathCase, isAbsolutePath } from 'vs/base/common/resources'; @@ -234,6 +234,7 @@ export class FileService extends Disposable implements IFileService { mtime: stat.mtime, ctime: stat.ctime, size: stat.size, + readonly: Boolean((stat.permissions ?? 0) & FilePermission.Readonly) || Boolean(provider.capabilities & FileSystemProviderCapabilities.Readonly), etag: etag({ mtime: stat.mtime, size: stat.size }) }; @@ -401,6 +402,9 @@ export class FileService extends Disposable implements IFileService { throw new FileOperationError(localize('fileIsDirectoryWriteError', "Unable to write file '{0}' that is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options); } + // File cannot be readonly + this.throwIfFileIsReadonly(resource, stat); + // Dirty write prevention: if the file on disk has been changed and does not match our expected // mtime and etag, we bail out to prevent dirty writing. // @@ -526,7 +530,14 @@ export class FileService extends Disposable implements IFileService { await consumeStream(fileStream); } - throw new FileOperationError(localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options); + // Re-throw errors as file operation errors but preserve + // specific errors (such as not modified since) + const message = localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()); + if (error instanceof NotModifiedSinceFileOperationError) { + throw new NotModifiedSinceFileOperationError(message, error.stat, options); + } else { + throw new FileOperationError(message, toFileOperationResult(error), options); + } } } @@ -594,7 +605,7 @@ export class FileService extends Disposable implements IFileService { // Throw if file not modified since (unless disabled) if (options && typeof options.etag === 'string' && options.etag !== ETAG_DISABLED && options.etag === stat.etag) { - throw new FileOperationError(localize('fileNotModifiedError', "File not modified since"), FileOperationResult.FILE_NOT_MODIFIED_SINCE, options); + throw new NotModifiedSinceFileOperationError(localize('fileNotModifiedError', "File not modified since"), stat, options); } // Throw if file is too large to load @@ -912,14 +923,22 @@ export class FileService extends Disposable implements IFileService { } // Validate delete - const exists = await this.exists(resource); - if (!exists) { + let stat: IStat | undefined = undefined; + try { + stat = await provider.stat(resource); + } catch (error) { + // Handled later + } + + if (stat) { + this.throwIfFileIsReadonly(resource, stat); + } else { throw new FileOperationError(localize('deleteFailedNotFound', "Unable to delete non-existing file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_NOT_FOUND); } // Validate recursive const recursive = !!options?.recursive; - if (!recursive && exists) { + if (!recursive) { const stat = await this.resolve(resource); if (stat.isDirectory && Array.isArray(stat.children) && stat.children.length > 0) { throw new Error(localize('deleteFailedNonEmptyFolder', "Unable to delete non-empty folder '{0}'.", this.resourceForError(resource))); @@ -1227,6 +1246,12 @@ export class FileService extends Disposable implements IFileService { return provider; } + private throwIfFileIsReadonly(resource: URI, stat: IStat): void { + if ((stat.permissions ?? 0) & FilePermission.Readonly) { + throw new FileOperationError(localize('err.readonly', "Unable to modify readonly file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_PERMISSION_DENIED); + } + } + private resourceForError(resource: URI): string { if (resource.scheme === Schemas.file) { return resource.fsPath; diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 414e8af3d7..1c66e234c0 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -11,7 +11,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { Event } from 'vs/base/common/event'; import { startsWithIgnoreCase } from 'vs/base/common/strings'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { isNumber, isUndefinedOrNull } from 'vs/base/common/types'; +import { isNumber } from 'vs/base/common/types'; import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; import { ReadableStreamEvents } from 'vs/base/common/stream'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -315,6 +315,14 @@ export enum FileType { SymbolicLink = 64 } +export enum FilePermission { + + /** + * File is readonly. + */ + Readonly = 1 +} + export interface IStat { /** @@ -335,7 +343,12 @@ export interface IStat { /** * The size of the file in bytes. */ - size: number; + readonly size: number; + + /** + * The file permissions. + */ + readonly permissions?: FilePermission; } export interface IWatchOptions { @@ -400,7 +413,7 @@ export interface IFileSystemProvider { readonly capabilities: FileSystemProviderCapabilities; readonly onDidChangeCapabilities: Event; - readonly onDidErrorOccur?: Event; // TODO@bpasero remove once file watchers are solid + readonly onDidErrorOccur?: Event; readonly onDidChangeFile: Event; watch(resource: URI, opts: IWatchOptions): IDisposable; @@ -475,7 +488,7 @@ export enum FileSystemProviderErrorCode { export class FileSystemProviderError extends Error { - constructor(message: string, public readonly code: FileSystemProviderErrorCode) { + constructor(message: string, readonly code: FileSystemProviderErrorCode) { super(message); } } @@ -592,7 +605,7 @@ export class FileOperationEvent { constructor(resource: URI, operation: FileOperation.DELETE); constructor(resource: URI, operation: FileOperation.CREATE | FileOperation.MOVE | FileOperation.COPY, target: IFileStatWithMetadata); - constructor(public readonly resource: URI, public readonly operation: FileOperation, public readonly target?: IFileStatWithMetadata) { } + constructor(readonly resource: URI, readonly operation: FileOperation, readonly target?: IFileStatWithMetadata) { } isOperation(operation: FileOperation.DELETE): boolean; isOperation(operation: FileOperation.MOVE | FileOperation.COPY | FileOperation.CREATE): this is { readonly target: IFileStatWithMetadata }; @@ -762,16 +775,6 @@ export class FileChangesEvent { return !!this.deleted; } - /** - * @deprecated use the `contains()` method to efficiently find out if the event - * relates to a given resource. this method ensures: - * - that there is no expensive lookup needed by using a `TernarySearchTree` - * - correctly handles `FileChangeType.DELETED` events - */ - getUpdated(): IFileChange[] { - return this.getOfType(FileChangeType.UPDATED); - } - /** * Returns if this event contains updated files. */ @@ -791,16 +794,6 @@ export class FileChangesEvent { return changes; } - - /** - * @deprecated use the `contains()` method to efficiently find out if the event - * relates to a given resource. this method ensures: - * - that there is no expensive lookup needed by using a `TernarySearchTree` - * - correctly handles `FileChangeType.DELETED` events - */ - filter(filterFn: (change: IFileChange) => boolean): FileChangesEvent { - return new FileChangesEvent(this.changes.filter(change => filterFn(change)), this.ignorePathCasing); - } } export function isParent(path: string, candidate: string, ignoreCase?: boolean): boolean { @@ -868,6 +861,11 @@ interface IBaseStat { * it is optional. */ readonly etag?: string; + + /** + * The file is read-only. + */ + readonly readonly?: boolean; } export interface IBaseStatWithMetadata extends Required { } @@ -906,6 +904,7 @@ export interface IFileStatWithMetadata extends IFileStat, IBaseStatWithMetadata readonly ctime: number; readonly etag: string; readonly size: number; + readonly readonly: boolean; readonly children?: IFileStatWithMetadata[]; } @@ -956,7 +955,7 @@ export interface IReadFileOptions extends IBaseReadFileOptions { * * Typically you should not need to use this flag but if * for example you are quickly reading a file right after - * a file event occured and the file changes a lot, there + * a file event occurred and the file changes a lot, there * is a chance that a read returns an empty or partial file * because a pending write has not finished yet. * @@ -1019,12 +1018,23 @@ export interface ICreateFileOptions { } export class FileOperationError extends Error { - constructor(message: string, public fileOperationResult: FileOperationResult, public options?: IReadFileOptions & IWriteFileOptions & ICreateFileOptions) { + constructor( + message: string, + readonly fileOperationResult: FileOperationResult, + readonly options?: IReadFileOptions & IWriteFileOptions & ICreateFileOptions + ) { super(message); } +} - static isFileOperationError(obj: unknown): obj is FileOperationError { - return obj instanceof Error && !isUndefinedOrNull((obj as FileOperationError).fileOperationResult); +export class NotModifiedSinceFileOperationError extends FileOperationError { + + constructor( + message: string, + readonly stat: IFileStatWithMetadata, + options?: IReadFileOptions + ) { + super(message, FileOperationResult.FILE_NOT_MODIFIED_SINCE, options); } } diff --git a/src/vs/platform/files/common/ipcFileSystemProvider.ts b/src/vs/platform/files/common/ipcFileSystemProvider.ts index 6e938b0797..2390fcd296 100644 --- a/src/vs/platform/files/common/ipcFileSystemProvider.ts +++ b/src/vs/platform/files/common/ipcFileSystemProvider.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Emitter } from 'vs/base/common/event'; diff --git a/src/vs/platform/files/electron-browser/diskFileSystemProvider.ts b/src/vs/platform/files/electron-browser/diskFileSystemProvider.ts index c18c0d6e57..77ee239cf9 100644 --- a/src/vs/platform/files/electron-browser/diskFileSystemProvider.ts +++ b/src/vs/platform/files/electron-browser/diskFileSystemProvider.ts @@ -3,11 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; +import { isWindows } from 'vs/base/common/platform'; +import { basename } from 'vs/base/common/path'; import { DiskFileSystemProvider as NodeDiskFileSystemProvider, IDiskFileSystemProviderOptions } from 'vs/platform/files/node/diskFileSystemProvider'; import { FileDeleteOptions, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; -import { isWindows } from 'vs/base/common/platform'; -import { localize } from 'vs/nls'; -import { basename } from 'vs/base/common/path'; import { ILogService } from 'vs/platform/log/common/log'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; @@ -34,8 +34,11 @@ export class DiskFileSystemProvider extends NodeDiskFileSystemProvider { return super.doDelete(filePath, opts); } - const result = await this.nativeHostService.moveItemToTrash(filePath); - if (!result) { + try { + await this.nativeHostService.moveItemToTrash(filePath); + } catch (error) { + this.logService.error(error); + throw new Error(isWindows ? localize('binFailed', "Failed to move '{0}' to the recycle bin", basename(filePath)) : localize('trashFailed', "Failed to move '{0}' to the trash", basename(filePath))); } } diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index 4099cdbef7..eec86a9694 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -3,14 +3,13 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { open, close, read, write, fdatasync, Stats, promises } from 'fs'; -import { promisify } from 'util'; +import { Stats } from 'fs'; 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, isFileOpenForWriteOptions } 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 { SymlinkSupport, move, copy, rimraf, RimRafMode, exists, readdir, IDirent } from 'vs/base/node/pfs'; +import { SymlinkSupport, RimRafMode, IDirent, Promises } 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'; @@ -47,7 +46,7 @@ export class DiskFileSystemProvider extends Disposable implements private readonly BUFFER_SIZE = this.options?.bufferSize || 64 * 1024; constructor( - private readonly logService: ILogService, + protected readonly logService: ILogService, private readonly options?: IDiskFileSystemProviderOptions ) { super(); @@ -96,7 +95,7 @@ export class DiskFileSystemProvider extends Disposable implements async readdir(resource: URI): Promise<[string, FileType][]> { try { - const children = await readdir(this.toFilePath(resource), { withFileTypes: true }); + const children = await Promises.readdir(this.toFilePath(resource), { withFileTypes: true }); const result: [string, FileType][] = []; await Promise.all(children.map(async child => { @@ -152,7 +151,7 @@ export class DiskFileSystemProvider extends Disposable implements try { const filePath = this.toFilePath(resource); - return await promises.readFile(filePath); + return await Promises.readFile(filePath); } catch (error) { throw this.toFileSystemProviderError(error); } @@ -176,7 +175,7 @@ export class DiskFileSystemProvider extends Disposable implements // Validate target unless { create: true, overwrite: true } if (!opts.create || !opts.overwrite) { - const fileExists = await exists(filePath); + const fileExists = await Promises.exists(filePath); if (fileExists) { if (!opts.overwrite) { throw createFileSystemProviderError(localize('fileExists', "File already exists"), FileSystemProviderErrorCode.FileExists); @@ -216,7 +215,7 @@ export class DiskFileSystemProvider extends Disposable implements try { const { stat } = await SymlinkSupport.stat(filePath); if (!(stat.mode & 0o200 /* File mode indicating writable by owner */)) { - await promises.chmod(filePath, stat.mode | 0o200); + await Promises.chmod(filePath, stat.mode | 0o200); } } catch (error) { this.logService.trace(error); // ignore any errors here and try to just write @@ -232,7 +231,7 @@ export class DiskFileSystemProvider extends Disposable implements // by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows // (see https://github.com/microsoft/vscode/issues/931) and prevent removing alternate data streams // (see https://github.com/microsoft/vscode/issues/6363) - await promises.truncate(filePath, 0); + await Promises.truncate(filePath, 0); // After a successful truncate() the flag can be set to 'r+' which will not truncate. flags = 'r+'; @@ -256,7 +255,7 @@ export class DiskFileSystemProvider extends Disposable implements flags = 'r'; } - const handle = await promisify(open)(filePath, flags); + const handle = await Promises.open(filePath, flags); // remember this handle to track file position of the handle // we init the position to 0 since the file descriptor was @@ -290,7 +289,7 @@ export class DiskFileSystemProvider extends Disposable implements // to flush the contents to disk if possible. if (this.writeHandles.delete(fd) && this.canFlush) { try { - await promisify(fdatasync)(fd); + await Promises.fdatasync(fd); } catch (error) { // In some exotic setups it is well possible that node fails to sync // In that case we disable flushing and log the error to our logger @@ -299,7 +298,7 @@ export class DiskFileSystemProvider extends Disposable implements } } - return await promisify(close)(fd); + return await Promises.close(fd); } catch (error) { throw this.toFileSystemProviderError(error); } @@ -310,7 +309,7 @@ export class DiskFileSystemProvider extends Disposable implements let bytesRead: number | null = null; try { - const result = await promisify(read)(fd, data, offset, length, normalizedPos); + const result = await Promises.read(fd, data, offset, length, normalizedPos); if (typeof result === 'number') { bytesRead = result; // node.d.ts fail @@ -396,7 +395,7 @@ export class DiskFileSystemProvider extends Disposable implements let bytesWritten: number | null = null; try { - const result = await promisify(write)(fd, data, offset, length, normalizedPos); + const result = await Promises.write(fd, data, offset, length, normalizedPos); if (typeof result === 'number') { bytesWritten = result; // node.d.ts fail @@ -418,7 +417,7 @@ export class DiskFileSystemProvider extends Disposable implements async mkdir(resource: URI): Promise { try { - await promises.mkdir(this.toFilePath(resource)); + await Promises.mkdir(this.toFilePath(resource)); } catch (error) { throw this.toFileSystemProviderError(error); } @@ -436,9 +435,9 @@ export class DiskFileSystemProvider extends Disposable implements protected async doDelete(filePath: string, opts: FileDeleteOptions): Promise { if (opts.recursive) { - await rimraf(filePath, RimRafMode.MOVE); + await Promises.rm(filePath, RimRafMode.MOVE); } else { - await promises.unlink(filePath); + await Promises.unlink(filePath); } } @@ -456,7 +455,7 @@ export class DiskFileSystemProvider extends Disposable implements await this.validateTargetDeleted(from, to, 'move', opts.overwrite); // Move - await move(fromFilePath, toFilePath); + await Promises.move(fromFilePath, toFilePath); } catch (error) { // rewrite some typical errors that can happen especially around symlinks @@ -483,7 +482,7 @@ export class DiskFileSystemProvider extends Disposable implements await this.validateTargetDeleted(from, to, 'copy', opts.overwrite); // Copy - await copy(fromFilePath, toFilePath, { preserveSymlinks: true }); + await Promises.copy(fromFilePath, toFilePath, { preserveSymlinks: true }); } catch (error) { // rewrite some typical errors that can happen especially around symlinks @@ -511,7 +510,7 @@ export class DiskFileSystemProvider extends Disposable implements } // handle existing target (unless this is a case change) - if (!isSameResourceWithDifferentPathCase && await exists(toFilePath)) { + if (!isSameResourceWithDifferentPathCase && await Promises.exists(toFilePath)) { if (!overwrite) { throw createFileSystemProviderError(localize('fileCopyErrorExists', "File at target already exists"), FileSystemProviderErrorCode.FileExists); } @@ -542,7 +541,7 @@ export class DiskFileSystemProvider extends Disposable implements return this.watchRecursive(resource, opts.excludes); } - return this.watchNonRecursive(resource); // TODO@bpasero ideally the same watcher can be used in both cases + return this.watchNonRecursive(resource); } private watchRecursive(resource: URI, excludes: string[]): IDisposable { diff --git a/src/vs/platform/files/test/browser/indexedDBFileService.test.ts b/src/vs/platform/files/test/browser/indexedDBFileService.test.ts index 53c8be8f3a..bf51814b45 100644 --- a/src/vs/platform/files/test/browser/indexedDBFileService.test.ts +++ b/src/vs/platform/files/test/browser/indexedDBFileService.test.ts @@ -14,11 +14,9 @@ import { IIndexedDBFileSystemProvider, IndexedDB, INDEXEDDB_LOGS_OBJECT_STORE, I import { assertIsDefined } from 'vs/base/common/types'; import { basename, joinPath } from 'vs/base/common/resources'; import { bufferToReadable, bufferToStream, VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; +import { flakySuite } from 'vs/base/test/common/testUtils'; -suite('IndexedDB File Service', function () { - - // IDB sometimes under pressure in build machines. - this.retries(3); +flakySuite('IndexedDB File Service', function () { const logSchema = 'logs'; diff --git a/src/vs/platform/files/test/common/files.test.ts b/src/vs/platform/files/test/common/files.test.ts index 9cd0a17f95..a5a75ebd67 100644 --- a/src/vs/platform/files/test/common/files.test.ts +++ b/src/vs/platform/files/test/common/files.test.ts @@ -60,7 +60,6 @@ suite('Files', () => { assert.strictEqual(6, event.changes.length); assert.strictEqual(1, event.getAdded().length); assert.strictEqual(true, event.gotAdded()); - assert.strictEqual(2, event.getUpdated().length); assert.strictEqual(true, event.gotUpdated()); assert.strictEqual(ignorePathCasing ? 2 : 3, event.getDeleted().length); assert.strictEqual(true, event.gotDeleted()); @@ -102,9 +101,6 @@ suite('Files', () => { case FileChangeType.ADDED: assert.strictEqual(8, event.getAdded().length); break; - case FileChangeType.UPDATED: - assert.strictEqual(8, event.getUpdated().length); - break; case FileChangeType.DELETED: assert.strictEqual(8, event.getDeleted().length); break; 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 bda21771f9..69a5ee611f 100644 --- a/src/vs/platform/files/test/electron-browser/diskFileService.test.ts +++ b/src/vs/platform/files/test/electron-browser/diskFileService.test.ts @@ -10,10 +10,10 @@ import { Schemas } from 'vs/base/common/network'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { getRandomTestPath, getPathFromAmdModule } from 'vs/base/test/node/testUtils'; import { join, basename, dirname, posix } from 'vs/base/common/path'; -import { copy, rimraf, rimrafSync } from 'vs/base/node/pfs'; +import { Promises, rimrafSync } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; -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, IReadFileOptions } from 'vs/platform/files/common/files'; +import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, createReadStream } from 'fs'; +import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities, FileChangeType, IFileChange, FileChangesEvent, FileOperationError, etag, IStat, IFileStatWithMetadata, IReadFileOptions, FilePermission, NotModifiedSinceFileOperationError } from 'vs/platform/files/common/files'; import { NullLogService } from 'vs/platform/log/common/log'; import { isLinux, isWindows } from 'vs/base/common/platform'; import { DisposableStore } from 'vs/base/common/lifecycle'; @@ -56,6 +56,7 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider { private invalidStatSize: boolean = false; private smallStatSize: boolean = false; + private readonly: boolean = false; private _testCapabilities!: FileSystemProviderCapabilities; override get capabilities(): FileSystemProviderCapabilities { @@ -88,13 +89,19 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider { this.smallStatSize = enabled; } + setReadonly(readonly: boolean): void { + this.readonly = readonly; + } + override async stat(resource: URI): Promise { const res = await super.stat(resource); if (this.invalidStatSize) { - res.size = String(res.size) as any; // for https://github.com/microsoft/vscode/issues/72909 + (res as any).size = String(res.size) as any; // for https://github.com/microsoft/vscode/issues/72909 } else if (this.smallStatSize) { - res.size = 1; + (res as any).size = 1; + } else if (this.readonly) { + (res as any).permissions = FilePermission.Readonly; } return res; @@ -147,13 +154,13 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const sourceDir = getPathFromAmdModule(require, './fixtures/service'); - await copy(sourceDir, testDir, { preserveSymlinks: false }); + await Promises.copy(sourceDir, testDir, { preserveSymlinks: false }); }); teardown(() => { disposables.clear(); - return rimraf(testDir); + return Promises.rm(testDir); }); test('createFolder', async () => { @@ -213,6 +220,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ assert.strictEqual(resolved.name, 'index.html'); assert.strictEqual(resolved.isFile, true); assert.strictEqual(resolved.isDirectory, false); + assert.strictEqual(resolved.readonly, false); assert.strictEqual(resolved.isSymbolicLink, false); assert.strictEqual(resolved.resource.toString(), resource.toString()); assert.strictEqual(resolved.children, undefined); @@ -233,6 +241,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ assert.ok(result.children); assert.ok(result.children!.length > 0); assert.ok(result!.isDirectory); + assert.strictEqual(result.readonly, false); assert.ok(result.mtime! > 0); assert.ok(result.ctime! > 0); assert.strictEqual(result.children!.length, testsElements.length); @@ -408,7 +417,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ test('resolve - folder symbolic link', async () => { const link = URI.file(join(testDir, 'deep-link')); - await promises.symlink(join(testDir, 'deep'), link.fsPath, 'junction'); + await Promises.symlink(join(testDir, 'deep'), link.fsPath, 'junction'); const resolved = await service.resolve(link); assert.strictEqual(resolved.children!.length, 4); @@ -418,7 +427,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('resolve - file symbolic link', async () => { const link = URI.file(join(testDir, 'lorem.txt-linked')); - await promises.symlink(join(testDir, 'lorem.txt'), link.fsPath); + await Promises.symlink(join(testDir, 'lorem.txt'), link.fsPath); const resolved = await service.resolve(link); assert.strictEqual(resolved.isDirectory, false); @@ -426,7 +435,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ }); test('resolve - symbolic link pointing to non-existing file does not break', async () => { - await promises.symlink(join(testDir, 'foo'), join(testDir, 'bar'), 'junction'); + await Promises.symlink(join(testDir, 'foo'), join(testDir, 'bar'), 'junction'); const resolved = await service.resolve(URI.file(testDir)); assert.strictEqual(resolved.isDirectory, true); @@ -477,7 +486,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (exists)', async () => { const target = URI.file(join(testDir, 'lorem.txt')); const link = URI.file(join(testDir, 'lorem.txt-linked')); - await promises.symlink(target.fsPath, link.fsPath); + await Promises.symlink(target.fsPath, link.fsPath); const source = await service.resolve(link); @@ -499,7 +508,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ (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 promises.symlink(target.fsPath, link.fsPath); + await Promises.symlink(target.fsPath, link.fsPath); let event: FileOperationEvent; disposables.add(service.onDidRunOperation(e => event = e)); @@ -1482,6 +1491,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ assert.ok(error); assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_NOT_MODIFIED_SINCE); + assert.ok(error instanceof NotModifiedSinceFileOperationError && error.stat); assert.strictEqual(fileProvider.totalBytesRead, 0); } @@ -1592,7 +1602,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('readFile - dangling symbolic link - https://github.com/microsoft/vscode/issues/116049', async () => { const link = URI.file(join(testDir, 'small.js-link')); - await promises.symlink(join(testDir, 'small.js'), link.fsPath); + await Promises.symlink(join(testDir, 'small.js'), link.fsPath); let error: FileOperationError | undefined = undefined; try { @@ -1928,8 +1938,8 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ await service.writeFile(lockedFile, VSBuffer.fromString('Locked File')); - const stats = await promises.stat(lockedFile.fsPath); - await promises.chmod(lockedFile.fsPath, stats.mode & ~0o200); + const stats = await Promises.stat(lockedFile.fsPath); + await Promises.chmod(lockedFile.fsPath, stats.mode & ~0o200); let error; const newContent = 'Updates to locked file'; @@ -2091,7 +2101,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ (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 promises.symlink(join(testDir, 'lorem.txt'), toWatch.fsPath); + await Promises.symlink(join(testDir, 'lorem.txt'), toWatch.fsPath); const promise = assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]]); setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes'), 50); @@ -2219,7 +2229,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ (runWatchTests ? test : test.skip)('watch - folder (non recursive) - symbolic link - change file', async () => { const watchDir = URI.file(join(testDir, 'deep-link')); - await promises.symlink(join(testDir, 'deep'), watchDir.fsPath, 'junction'); + await Promises.symlink(join(testDir, 'deep'), watchDir.fsPath, 'junction'); const file = URI.file(join(watchDir.fsPath, 'index.html')); writeFileSync(file.fsPath, 'Init'); @@ -2390,4 +2400,32 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ await fileProvider.close(fdWrite); await fileProvider.close(fdRead); }); + + test('readonly - is handled properly for a single resource', async () => { + fileProvider.setReadonly(true); + + const resource = URI.file(join(testDir, 'index.html')); + + const resolveResult = await service.resolve(resource); + assert.strictEqual(resolveResult.readonly, true); + + const readResult = await service.readFile(resource); + assert.strictEqual(readResult.readonly, true); + + let writeFileError: Error | undefined = undefined; + try { + await service.writeFile(resource, VSBuffer.fromString('Hello Test')); + } catch (error) { + writeFileError = error; + } + assert.ok(writeFileError); + + let deleteFileError: Error | undefined = undefined; + try { + await service.del(resource); + } catch (error) { + deleteFileError = error; + } + assert.ok(deleteFileError); + }); }); diff --git a/src/vs/platform/files/test/electron-browser/fixtures/resolver/site.css b/src/vs/platform/files/test/electron-browser/fixtures/resolver/site.css index b7e5283202..c5cea74684 100644 --- a/src/vs/platform/files/test/electron-browser/fixtures/resolver/site.css +++ b/src/vs/platform/files/test/electron-browser/fixtures/resolver/site.css @@ -25,12 +25,12 @@ h1, h2, h3, h4, h5, h6 margin: 0px; } -textarea +textarea { font-family: Consolas } -#results +#results { margin-top: 2em; margin-left: 2em; diff --git a/src/vs/platform/ipc/electron-browser/mainProcessService.ts b/src/vs/platform/ipc/electron-browser/mainProcessService.ts index 276420e848..d1c1164c28 100644 --- a/src/vs/platform/ipc/electron-browser/mainProcessService.ts +++ b/src/vs/platform/ipc/electron-browser/mainProcessService.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IChannel, IServerChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; diff --git a/src/vs/platform/ipc/electron-sandbox/services.ts b/src/vs/platform/ipc/electron-sandbox/services.ts index 498136177d..4be2b2c495 100644 --- a/src/vs/platform/ipc/electron-sandbox/services.ts +++ b/src/vs/platform/ipc/electron-sandbox/services.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IChannel, IServerChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; diff --git a/src/vs/platform/issue/common/issue.ts b/src/vs/platform/issue/common/issue.ts index e02a859f91..482e783b04 100644 --- a/src/vs/platform/issue/common/issue.ts +++ b/src/vs/platform/issue/common/issue.ts @@ -58,6 +58,7 @@ export interface IssueReporterData extends WindowData { issueType?: IssueType; extensionId?: string; experiments?: string; + restrictedMode: boolean; githubAccessToken: string; readonly issueTitle?: string; readonly issueBody?: string; @@ -70,8 +71,14 @@ export interface ISettingSearchResult { } export interface ProcessExplorerStyles extends WindowStyles { - hoverBackground?: string; - hoverForeground?: string; + listHoverBackground?: string; + listHoverForeground?: string; + listFocusBackground?: string; + listFocusForeground?: string; + listFocusOutline?: string; + listActiveSelectionBackground?: string; + listActiveSelectionForeground?: string; + listHoverOutline?: 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 149662149b..e12a79127d 100644 --- a/src/vs/platform/issue/electron-main/issueMainService.ts +++ b/src/vs/platform/issue/electron-main/issueMainService.ts @@ -25,6 +25,13 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; export const IIssueMainService = createDecorator('issueMainService'); +interface IBrowserWindowOptions { + backgroundColor: string | undefined; + title: string; + zoomLevel: number; + alwaysOnTop: boolean; +} + export interface IIssueMainService extends ICommonIssueService { } export class IssueMainService implements ICommonIssueService { @@ -189,7 +196,12 @@ export class IssueMainService implements ICommonIssueService { const issueReporterWindowConfigUrl = issueReporterDisposables.add(this.protocolMainService.createIPCObjectUrl()); const position = this.getWindowPosition(this.issueReporterParentWindow, 700, 800); - this.issueReporterWindow = this.createBrowserWindow(position, issueReporterWindowConfigUrl, data.styles.backgroundColor, localize('issueReporter', "Issue Reporter"), data.zoomLevel); + this.issueReporterWindow = this.createBrowserWindow(position, issueReporterWindowConfigUrl, { + backgroundColor: data.styles.backgroundColor, + title: localize('issueReporter', "Issue Reporter"), + zoomLevel: data.zoomLevel, + alwaysOnTop: false + }); // Store into config object URL issueReporterWindowConfigUrl.update({ @@ -239,7 +251,12 @@ export class IssueMainService implements ICommonIssueService { const processExplorerWindowConfigUrl = processExplorerDisposables.add(this.protocolMainService.createIPCObjectUrl()); const position = this.getWindowPosition(this.processExplorerParentWindow, 800, 500); - this.processExplorerWindow = this.createBrowserWindow(position, processExplorerWindowConfigUrl, data.styles.backgroundColor, localize('processExplorer', "Process Explorer"), data.zoomLevel); + this.processExplorerWindow = this.createBrowserWindow(position, processExplorerWindowConfigUrl, { + backgroundColor: data.styles.backgroundColor, + title: localize('processExplorer', "Process Explorer"), + zoomLevel: data.zoomLevel, + alwaysOnTop: true + }); // Store into config object URL processExplorerWindowConfigUrl.update({ @@ -273,7 +290,7 @@ export class IssueMainService implements ICommonIssueService { this.processExplorerWindow?.focus(); } - private createBrowserWindow(position: IWindowState, ipcObjectUrl: IIPCObjectUrl, backgroundColor: string | undefined, title: string, zoomLevel: number): BrowserWindow { + private createBrowserWindow(position: IWindowState, ipcObjectUrl: IIPCObjectUrl, options: IBrowserWindowOptions): BrowserWindow { const window = new BrowserWindow({ fullscreen: false, skipTaskbar: true, @@ -284,20 +301,20 @@ export class IssueMainService implements ICommonIssueService { minHeight: 200, x: position.x, y: position.y, - title, - backgroundColor: backgroundColor || IssueMainService.DEFAULT_BACKGROUND_COLOR, + title: options.title, + backgroundColor: options.backgroundColor || IssueMainService.DEFAULT_BACKGROUND_COLOR, webPreferences: { preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, additionalArguments: [`--vscode-window-config=${ipcObjectUrl.resource.toString()}`, '--context-isolation' /* TODO@bpasero: Use process.contextIsolateed when 13-x-y is adopted (https://github.com/electron/electron/pull/28030) */], v8CacheOptions: browserCodeLoadingCacheStrategy, enableWebSQL: false, - enableRemoteModule: false, spellcheck: false, nativeWindowOpen: true, - zoomFactor: zoomLevelToZoomFactor(zoomLevel), + zoomFactor: zoomLevelToZoomFactor(options.zoomLevel), sandbox: true, - contextIsolation: true - } + contextIsolation: true, + }, + alwaysOnTop: options.alwaysOnTop }); window.setMenuBarVisibility(false); diff --git a/src/vs/platform/jsonschemas/common/jsonContributionRegistry.ts b/src/vs/platform/jsonschemas/common/jsonContributionRegistry.ts index f9a56e29dc..35ab2e61c0 100644 --- a/src/vs/platform/jsonschemas/common/jsonContributionRegistry.ts +++ b/src/vs/platform/jsonschemas/common/jsonContributionRegistry.ts @@ -77,4 +77,4 @@ class JSONContributionRegistry implements IJSONContributionRegistry { } const jsonContributionRegistry = new JSONContributionRegistry(); -platform.Registry.add(Extensions.JSONContribution, jsonContributionRegistry); \ No newline at end of file +platform.Registry.add(Extensions.JSONContribution, jsonContributionRegistry); diff --git a/src/vs/platform/keybinding/common/abstractKeybindingService.ts b/src/vs/platform/keybinding/common/abstractKeybindingService.ts index 7cd0e07cf2..50ac290780 100644 --- a/src/vs/platform/keybinding/common/abstractKeybindingService.ts +++ b/src/vs/platform/keybinding/common/abstractKeybindingService.ts @@ -24,6 +24,9 @@ interface CurrentChord { label: string | null; } +// Skip logging for high-frequency text editing commands +const HIGH_FREQ_COMMANDS = /^(cursor|delete)/; + export abstract class AbstractKeybindingService extends Disposable implements IKeybindingService { public _serviceBrand: undefined; @@ -107,8 +110,8 @@ export abstract class AbstractKeybindingService extends Disposable implements IK ); } - public lookupKeybinding(commandId: string): ResolvedKeybinding | undefined { - const result = this._getResolver().lookupPrimaryKeybinding(commandId); + public lookupKeybinding(commandId: string, context?: IContextKeyService): ResolvedKeybinding | undefined { + const result = this._getResolver().lookupPrimaryKeybinding(commandId, context); if (!result) { return undefined; } @@ -263,7 +266,9 @@ export abstract class AbstractKeybindingService extends Disposable implements IK } else { this._commandService.executeCommand(resolveResult.commandId, resolveResult.commandArgs).then(undefined, err => this._notificationService.warn(err)); } - this._telemetryService.publicLog2('workbenchActionExecuted', { id: resolveResult.commandId, from: 'keybinding' }); + if (!HIGH_FREQ_COMMANDS.test(resolveResult.commandId)) { + this._telemetryService.publicLog2('workbenchActionExecuted', { id: resolveResult.commandId, from: 'keybinding' }); + } } return shouldPreventDefault; diff --git a/src/vs/platform/keybinding/common/keybinding.ts b/src/vs/platform/keybinding/common/keybinding.ts index b086e6e83d..88b1c79ad2 100644 --- a/src/vs/platform/keybinding/common/keybinding.ts +++ b/src/vs/platform/keybinding/common/keybinding.ts @@ -6,7 +6,7 @@ import { Event } from 'vs/base/common/event'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { Keybinding, KeyCode, ResolvedKeybinding } from 'vs/base/common/keyCodes'; -import { IContextKeyServiceTarget } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKeyServiceTarget } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IResolveResult } from 'vs/platform/keybinding/common/keybindingResolver'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; @@ -85,7 +85,7 @@ export interface IKeybindingService { * Look up the preferred (last defined) keybinding for a command. * @returns The preferred keybinding or null if the command is not bound. */ - lookupKeybinding(commandId: string): ResolvedKeybinding | undefined; + lookupKeybinding(commandId: string, context?: IContextKeyService): ResolvedKeybinding | undefined; getDefaultKeybindingsContent(): string; diff --git a/src/vs/platform/keybinding/common/keybindingResolver.ts b/src/vs/platform/keybinding/common/keybindingResolver.ts index d1202ee543..f6f56d37a6 100644 --- a/src/vs/platform/keybinding/common/keybindingResolver.ts +++ b/src/vs/platform/keybinding/common/keybindingResolver.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IContext, ContextKeyExpression, ContextKeyExprType } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpression, ContextKeyExprType, IContext, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; export interface IResolveResult { @@ -183,10 +183,10 @@ export class KeybindingResolver { * Returns true if it is provable `a` implies `b`. */ public static whenIsEntirelyIncluded(a: ContextKeyExpression | null | undefined, b: ContextKeyExpression | null | undefined): boolean { - if (!b) { + if (!b || b.type === ContextKeyExprType.True) { return true; } - if (!a) { + if (!a || a.type === ContextKeyExprType.True) { return false; } @@ -247,13 +247,15 @@ export class KeybindingResolver { return result; } - public lookupPrimaryKeybinding(commandId: string): ResolvedKeybindingItem | null { + public lookupPrimaryKeybinding(commandId: string, context?: IContextKeyService): ResolvedKeybindingItem | null { let items = this._lookupMap.get(commandId); if (typeof items === 'undefined' || items.length === 0) { return null; } - return items[items.length - 1]; + const itemMatchingContext = context && + Array.from(items).reverse().find(item => context.contextMatchesRules(item.when)); + return itemMatchingContext ?? items[items.length - 1]; } public resolve(context: IContext, currentChord: string | null, keypress: string): IResolveResult | null { diff --git a/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts b/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts index bac0d5f518..4506769e87 100644 --- a/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts +++ b/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts @@ -193,16 +193,27 @@ suite('KeybindingResolver', () => { }); test('contextIsEntirelyIncluded', () => { - const assertIsIncluded = (a: string | null, b: string | null) => { - assert.strictEqual(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), true); + const toContextKeyExpression = (expr: ContextKeyExpression | string | null) => { + if (typeof expr === 'string' || !expr) { + return ContextKeyExpr.deserialize(expr as string); // {{SQL CARBON EDIT}} Cast to string + } + return expr; }; - const assertIsNotIncluded = (a: string | null, b: string | null) => { - assert.strictEqual(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), false); + const assertIsIncluded = (a: ContextKeyExpression | string | null, b: ContextKeyExpression | string | null) => { + assert.strictEqual(KeybindingResolver.whenIsEntirelyIncluded(toContextKeyExpression(a), toContextKeyExpression(b)), true); + }; + const assertIsNotIncluded = (a: ContextKeyExpression | string | null, b: ContextKeyExpression | string | null) => { + assert.strictEqual(KeybindingResolver.whenIsEntirelyIncluded(toContextKeyExpression(a), toContextKeyExpression(b)), false); }; + assertIsIncluded(null, null); + assertIsIncluded(null, ContextKeyExpr.true()); + assertIsIncluded(ContextKeyExpr.true(), null); + assertIsIncluded(ContextKeyExpr.true(), ContextKeyExpr.true()); assertIsIncluded('key1', null); assertIsIncluded('key1', ''); assertIsIncluded('key1', 'key1'); + assertIsIncluded('key1', ContextKeyExpr.true()); assertIsIncluded('!key1', ''); assertIsIncluded('!key1', '!key1'); assertIsIncluded('key2', ''); diff --git a/src/vs/platform/keyboardLayout/common/dispatchConfig.ts b/src/vs/platform/keyboardLayout/common/dispatchConfig.ts index b462353e93..93ee1c3444 100644 --- a/src/vs/platform/keyboardLayout/common/dispatchConfig.ts +++ b/src/vs/platform/keyboardLayout/common/dispatchConfig.ts @@ -14,4 +14,4 @@ export function getDispatchConfig(configurationService: IConfigurationService): const keyboard = configurationService.getValue('keyboard'); const r = (keyboard ? (keyboard).dispatch : null); return (r === 'keyCode' ? DispatchConfig.KeyCode : DispatchConfig.Code); -} \ No newline at end of file +} diff --git a/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts b/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts index 06d7232e8c..77ad97de12 100644 --- a/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts +++ b/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts @@ -5,7 +5,7 @@ import { ipcMain, app, BrowserWindow } from 'electron'; import { ILogService } from 'vs/platform/log/common/log'; -import { IStateService } from 'vs/platform/state/node/state'; +import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { Event, Emitter } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ICodeWindow } from 'vs/platform/windows/electron-main/windows'; @@ -115,12 +115,12 @@ export interface ILifecycleMainService { /** * Restart the application with optional arguments (CLI). All lifecycle event handlers are triggered. */ - relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): void; + relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): Promise; /** * Shutdown the application normally. All lifecycle event handlers are triggered. */ - quit(fromUpdate?: boolean): Promise; + quit(willRestart?: boolean): Promise; /** * Forcefully shutdown the application. No livecycle event handlers are triggered. @@ -158,7 +158,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe declare readonly _serviceBrand: undefined; - private static readonly QUIT_FROM_RESTART_MARKER = 'quit.from.restart'; // use a marker to find out if the session was restarted + private static readonly QUIT_AND_RESTART_KEY = 'lifecycle.quitAndRestart'; private readonly _onBeforeShutdown = this._register(new Emitter()); readonly onBeforeShutdown = this._onBeforeShutdown.event; @@ -188,28 +188,29 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe private oneTimeListenerTokenGenerator = 0; private windowCounter = 0; - private pendingQuitPromise: Promise | null = null; - private pendingQuitPromiseResolve: { (veto: boolean): void } | null = null; + private pendingQuitPromise: Promise | undefined = undefined; + private pendingQuitPromiseResolve: { (veto: boolean): void } | undefined = undefined; - private pendingWillShutdownPromise: Promise | null = null; + private pendingWillShutdownPromise: Promise | undefined = undefined; private readonly phaseWhen = new Map(); constructor( @ILogService private readonly logService: ILogService, - @IStateService private readonly stateService: IStateService + @IStateMainService private readonly stateMainService: IStateMainService ) { super(); - this.handleRestarted(); + this.resolveRestarted(); this.when(LifecycleMainPhase.Ready).then(() => this.registerListeners()); } - private handleRestarted(): void { - this._wasRestarted = !!this.stateService.getItem(LifecycleMainService.QUIT_FROM_RESTART_MARKER); + private resolveRestarted(): void { + this._wasRestarted = !!this.stateMainService.getItem(LifecycleMainService.QUIT_AND_RESTART_KEY); if (this._wasRestarted) { - this.stateService.removeItem(LifecycleMainService.QUIT_FROM_RESTART_MARKER); // remove the marker right after if found + // remove the marker right after if found + this.stateMainService.removeItem(LifecycleMainService.QUIT_AND_RESTART_KEY); } } @@ -294,7 +295,23 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe } }); - this.pendingWillShutdownPromise = Promises.settled(joiners).then(() => undefined, err => this.logService.error(err)); + this.pendingWillShutdownPromise = (async () => { + + // Settle all shutdown event joiners + try { + await Promises.settled(joiners); + } catch (error) { + this.logService.error(error); + } + + // Then, always make sure at the end + // the state service is flushed. + try { + await this.stateMainService.close(); + } catch (error) { + this.logService.error(error); + } + })(); return this.pendingWillShutdownPromise; } @@ -454,8 +471,8 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe private resolvePendingQuitPromise(veto: boolean): void { if (this.pendingQuitPromiseResolve) { this.pendingQuitPromiseResolve(veto); - this.pendingQuitPromiseResolve = null; - this.pendingQuitPromise = null; + this.pendingQuitPromiseResolve = undefined; + this.pendingQuitPromise = undefined; } } @@ -502,16 +519,16 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe }); } - quit(fromUpdate?: boolean): Promise { + quit(willRestart?: boolean): Promise { if (this.pendingQuitPromise) { return this.pendingQuitPromise; } - this.logService.trace(`Lifecycle#quit() - from update: ${fromUpdate}`); + this.logService.trace(`Lifecycle#quit() - will restart: ${willRestart}`); - // Remember the reason for quit was to restart - if (fromUpdate) { - this.stateService.setItem(LifecycleMainService.QUIT_FROM_RESTART_MARKER, true); + // Remember if we are about to restart + if (willRestart) { + this.stateMainService.setItem(LifecycleMainService.QUIT_AND_RESTART_KEY, true); } this.pendingQuitPromise = new Promise(resolve => { @@ -528,7 +545,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe return this.pendingQuitPromise; } - relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): void { + async relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): Promise { this.logService.trace('Lifecycle#relaunch()'); const args = process.argv.slice(1); @@ -545,37 +562,34 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe } } - let quitVetoed = false; - app.once('quit', () => { - if (!quitVetoed) { - - // Remember the reason for quit was to restart - this.stateService.setItem(LifecycleMainService.QUIT_FROM_RESTART_MARKER, true); - - // Windows: we are about to restart and as such we need to restore the original - // current working directory we had on startup to get the exact same startup - // behaviour. As such, we briefly change back to that directory and then when - // Code starts it will set it back to the installation directory again. - try { - if (isWindows) { - const currentWorkingDir = cwd(); - if (currentWorkingDir !== process.cwd()) { - process.chdir(currentWorkingDir); - } + const quitListener = () => { + // Windows: we are about to restart and as such we need to restore the original + // current working directory we had on startup to get the exact same startup + // behaviour. As such, we briefly change back to that directory and then when + // Code starts it will set it back to the installation directory again. + try { + if (isWindows) { + const currentWorkingDir = cwd(); + if (currentWorkingDir !== process.cwd()) { + process.chdir(currentWorkingDir); } - } catch (err) { - this.logService.error(err); } - - // relaunch after we are sure there is no veto - this.logService.trace('Lifecycle#relaunch() - calling app.relaunch()'); - app.relaunch({ args }); + } catch (err) { + this.logService.error(err); } - }); + + // relaunch after we are sure there is no veto + this.logService.trace('Lifecycle#relaunch() - calling app.relaunch()'); + app.relaunch({ args }); + }; + app.once('quit', quitListener); // app.relaunch() does not quit automatically, so we quit first, // check for vetoes and then relaunch from the app.on('quit') event - this.quit().then(veto => quitVetoed = veto); + const veto = await this.quit(true /* will restart */); + if (veto) { + app.removeListener('quit', quitListener); + } } async kill(code?: number): Promise { diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index b5037a207d..2d54637bbc 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -183,7 +183,7 @@ function toWorkbenchListOptions(options: IListOptions, configurationServic } }; - result.smoothScrolling = configurationService.getValue(listSmoothScrolling); + result.smoothScrolling = Boolean(configurationService.getValue(listSmoothScrolling)); return [result, disposables]; } @@ -221,7 +221,7 @@ export class WorkbenchList extends List { @IConfigurationService configurationService: IConfigurationService, @IKeybindingService keybindingService: IKeybindingService ) { - const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : configurationService.getValue(horizontalScrollingKey); + const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : Boolean(configurationService.getValue(horizontalScrollingKey)); const [workbenchListOptions, workbenchListOptionsDisposable] = toWorkbenchListOptions(options, configurationService, keybindingService); super(user, container, delegate, renderers, @@ -282,11 +282,11 @@ export class WorkbenchList extends List { let options: IListOptionsUpdate = {}; if (e.affectsConfiguration(horizontalScrollingKey) && this.horizontalScrolling === undefined) { - const horizontalScrolling = configurationService.getValue(horizontalScrollingKey); + const horizontalScrolling = Boolean(configurationService.getValue(horizontalScrollingKey)); options = { ...options, horizontalScrolling }; } if (e.affectsConfiguration(listSmoothScrolling)) { - const smoothScrolling = configurationService.getValue(listSmoothScrolling); + const smoothScrolling = Boolean(configurationService.getValue(listSmoothScrolling)); options = { ...options, smoothScrolling }; } if (Object.keys(options).length > 0) { @@ -348,7 +348,7 @@ export class WorkbenchPagedList extends PagedList { @IConfigurationService configurationService: IConfigurationService, @IKeybindingService keybindingService: IKeybindingService ) { - const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : configurationService.getValue(horizontalScrollingKey); + const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : Boolean(configurationService.getValue(horizontalScrollingKey)); const [workbenchListOptions, workbenchListOptionsDisposable] = toWorkbenchListOptions(options, configurationService, keybindingService); super(user, container, delegate, renderers, { @@ -394,11 +394,11 @@ export class WorkbenchPagedList extends PagedList { let options: IListOptionsUpdate = {}; if (e.affectsConfiguration(horizontalScrollingKey) && this.horizontalScrolling === undefined) { - const horizontalScrolling = configurationService.getValue(horizontalScrollingKey); + const horizontalScrolling = Boolean(configurationService.getValue(horizontalScrollingKey)); options = { ...options, horizontalScrolling }; } if (e.affectsConfiguration(listSmoothScrolling)) { - const smoothScrolling = configurationService.getValue(listSmoothScrolling); + const smoothScrolling = Boolean(configurationService.getValue(listSmoothScrolling)); options = { ...options, smoothScrolling }; } if (Object.keys(options).length > 0) { @@ -469,7 +469,7 @@ export class WorkbenchTable extends Table { @IConfigurationService configurationService: IConfigurationService, @IKeybindingService keybindingService: IKeybindingService ) { - const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : configurationService.getValue(horizontalScrollingKey); + const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : Boolean(configurationService.getValue(horizontalScrollingKey)); const [workbenchListOptions, workbenchListOptionsDisposable] = toWorkbenchListOptions(options, configurationService, keybindingService); super(user, container, delegate, columns, renderers, @@ -531,11 +531,11 @@ export class WorkbenchTable extends Table { let options: IListOptionsUpdate = {}; if (e.affectsConfiguration(horizontalScrollingKey) && this.horizontalScrolling === undefined) { - const horizontalScrolling = configurationService.getValue(horizontalScrollingKey); + const horizontalScrolling = Boolean(configurationService.getValue(horizontalScrollingKey)); options = { ...options, horizontalScrolling }; } if (e.affectsConfiguration(listSmoothScrolling)) { - const smoothScrolling = configurationService.getValue(listSmoothScrolling); + const smoothScrolling = Boolean(configurationService.getValue(listSmoothScrolling)); options = { ...options, smoothScrolling }; } if (Object.keys(options).length > 0) { @@ -999,10 +999,10 @@ function workbenchTreeDataPreamble { // give priority to the context key value to disable this completely - let automaticKeyboardNavigation = contextKeyService.getContextKeyValue(WorkbenchListAutomaticKeyboardNavigationKey); + let automaticKeyboardNavigation = Boolean(contextKeyService.getContextKeyValue(WorkbenchListAutomaticKeyboardNavigationKey)); if (automaticKeyboardNavigation) { - automaticKeyboardNavigation = configurationService.getValue(automaticKeyboardNavigationSettingKey); + automaticKeyboardNavigation = Boolean(configurationService.getValue(automaticKeyboardNavigationSettingKey)); } return automaticKeyboardNavigation; @@ -1010,7 +1010,7 @@ function workbenchTreeDataPreamble(keyboardNavigationSettingKey); - const horizontalScrolling = options.horizontalScrolling !== undefined ? options.horizontalScrolling : configurationService.getValue(horizontalScrollingKey); + const horizontalScrolling = options.horizontalScrolling !== undefined ? options.horizontalScrolling : Boolean(configurationService.getValue(horizontalScrollingKey)); const [workbenchListOptions, disposable] = toWorkbenchListOptions(options, configurationService, keybindingService); const additionalScrollHeight = options.additionalScrollHeight; @@ -1023,7 +1023,7 @@ function workbenchTreeDataPreamble(treeIndentKey), renderIndentGuides: configurationService.getValue(treeRenderIndentGuidesKey), - smoothScrolling: configurationService.getValue(listSmoothScrolling), + smoothScrolling: Boolean(configurationService.getValue(listSmoothScrolling)), automaticKeyboardNavigation: getAutomaticKeyboardNavigation(), simpleKeyboardNavigation: keyboardNavigation === 'simple', filterOnType: keyboardNavigation === 'filter', @@ -1120,7 +1120,7 @@ class WorkbenchTreeInternals { newOptions = { ...newOptions, renderIndentGuides }; } if (e.affectsConfiguration(listSmoothScrolling)) { - const smoothScrolling = configurationService.getValue(listSmoothScrolling); + const smoothScrolling = Boolean(configurationService.getValue(listSmoothScrolling)); newOptions = { ...newOptions, smoothScrolling }; } if (e.affectsConfiguration(keyboardNavigationSettingKey)) { @@ -1130,7 +1130,7 @@ class WorkbenchTreeInternals { newOptions = { ...newOptions, automaticKeyboardNavigation: getAutomaticKeyboardNavigation() }; } if (e.affectsConfiguration(horizontalScrollingKey) && options.horizontalScrolling === undefined) { - const horizontalScrolling = configurationService.getValue(horizontalScrollingKey); + const horizontalScrolling = Boolean(configurationService.getValue(horizontalScrollingKey)); newOptions = { ...newOptions, horizontalScrolling }; } if (e.affectsConfiguration(treeExpandMode) && options.expandOnlyOnTwistieClick === undefined) { @@ -1171,20 +1171,20 @@ class WorkbenchTreeInternals { const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ - 'id': 'workbench', - 'order': 7, - 'title': localize('workbenchConfigurationTitle', "Workbench"), - 'type': 'object', - 'properties': { + id: 'workbench', + order: 7, + title: localize('workbenchConfigurationTitle', "Workbench"), + type: 'object', + properties: { [multiSelectModifierSettingKey]: { - 'type': 'string', - 'enum': ['ctrlCmd', 'alt'], - 'enumDescriptions': [ + type: 'string', + enum: ['ctrlCmd', 'alt'], + enumDescriptions: [ localize('multiSelectModifier.ctrlCmd', "Maps to `Control` on Windows and Linux and to `Command` on macOS."), localize('multiSelectModifier.alt', "Maps to `Alt` on Windows and Linux and to `Option` on macOS.") ], - 'default': 'ctrlCmd', - 'description': localize({ + default: 'ctrlCmd', + description: localize({ key: 'multiSelectModifier', comment: [ '- `ctrlCmd` refers to a value the setting can take and should not be localized.', @@ -1193,25 +1193,25 @@ configurationRegistry.registerConfiguration({ }, "The modifier to be used to add an item in trees and lists to a multi-selection with the mouse (for example in the explorer, open editors and scm view). The 'Open to Side' mouse gestures - if supported - will adapt such that they do not conflict with the multiselect modifier.") }, [openModeSettingKey]: { - 'type': 'string', - 'enum': ['singleClick', 'doubleClick'], - 'default': 'singleClick', - 'description': localize({ + type: 'string', + enum: ['singleClick', 'doubleClick'], + default: 'singleClick', + description: localize({ key: 'openModeModifier', comment: ['`singleClick` and `doubleClick` refers to a value the setting can take and should not be localized.'] }, "Controls how to open items in trees and lists using the mouse (if supported). Note that some trees and lists might choose to ignore this setting if it is not applicable.") }, [horizontalScrollingKey]: { - 'type': 'boolean', - 'default': false, - 'description': localize('horizontalScrolling setting', "Controls whether lists and trees support horizontal scrolling in the workbench. Warning: turning on this setting has a performance implication.") + type: 'boolean', + default: false, + description: localize('horizontalScrolling setting', "Controls whether lists and trees support horizontal scrolling in the workbench. Warning: turning on this setting has a performance implication.") }, [treeIndentKey]: { - 'type': 'number', - 'default': 8, + type: 'number', + default: 8, minimum: 0, maximum: 40, - 'description': localize('tree indent setting', "Controls tree indentation in pixels.") + description: localize('tree indent setting', "Controls tree indentation in pixels.") }, [treeRenderIndentGuidesKey]: { type: 'string', @@ -1225,19 +1225,19 @@ configurationRegistry.registerConfiguration({ description: localize('list smoothScrolling setting', "Controls whether lists and trees have smooth scrolling."), }, [keyboardNavigationSettingKey]: { - 'type': 'string', - 'enum': ['simple', 'highlight', 'filter'], - 'enumDescriptions': [ + type: 'string', + enum: ['simple', 'highlight', 'filter'], + enumDescriptions: [ localize('keyboardNavigationSettingKey.simple', "Simple keyboard navigation focuses elements which match the keyboard input. Matching is done only on prefixes."), localize('keyboardNavigationSettingKey.highlight', "Highlight keyboard navigation highlights elements which match the keyboard input. Further up and down navigation will traverse only the highlighted elements."), localize('keyboardNavigationSettingKey.filter', "Filter keyboard navigation will filter out and hide all the elements which do not match the keyboard input.") ], - 'default': 'highlight', - 'description': localize('keyboardNavigationSettingKey', "Controls the keyboard navigation style for lists and trees in the workbench. Can be simple, highlight and filter.") + default: 'highlight', + description: localize('keyboardNavigationSettingKey', "Controls the keyboard navigation style for lists and trees in the workbench. Can be simple, highlight and filter.") }, [automaticKeyboardNavigationSettingKey]: { - 'type': 'boolean', - 'default': true, + type: 'boolean', + 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.") }, [treeExpandMode]: { diff --git a/src/vs/platform/localizations/node/localizations.ts b/src/vs/platform/localizations/node/localizations.ts index c541b256c4..993b62e66e 100644 --- a/src/vs/platform/localizations/node/localizations.ts +++ b/src/vs/platform/localizations/node/localizations.ts @@ -3,8 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { writeFile } from 'vs/base/node/pfs'; -import { promises } from 'fs'; +import { Promises } from 'vs/base/node/pfs'; import { createHash } from 'crypto'; import { IExtensionManagementService, ILocalExtension, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -158,7 +157,7 @@ class LanguagePacksCache extends Disposable { private withLanguagePacks(fn: (languagePacks: { [language: string]: ILanguagePack }) => T | null = () => null): Promise { return this.languagePacksFileLimiter.queue(() => { let result: T | null = null; - return promises.readFile(this.languagePacksFilePath, 'utf8') + return 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; }) @@ -172,7 +171,7 @@ class LanguagePacksCache extends Disposable { this.initializedCache = true; const raw = JSON.stringify(this.languagePacks); this.logService.debug('Writing language packs', raw); - return writeFile(this.languagePacksFilePath, raw); + return Promises.writeFile(this.languagePacksFilePath, raw); }) .then(() => result, error => this.logService.error(error)); }); diff --git a/src/vs/platform/log/node/spdlogLog.ts b/src/vs/platform/log/node/spdlogLog.ts index 28c74782e1..a393416b18 100644 --- a/src/vs/platform/log/node/spdlogLog.ts +++ b/src/vs/platform/log/node/spdlogLog.ts @@ -7,20 +7,21 @@ import { LogLevel, ILogger, AbstractMessageLogger } from 'vs/platform/log/common import * as spdlog from 'spdlog'; import { ByteSize } from 'vs/platform/files/common/files'; -async function createSpdLogLogger(name: string, logfilePath: string, filesize: number, filecount: number): Promise { +async function createSpdLogLogger(name: string, logfilePath: string, filesize: number, filecount: number): Promise { // Do not crash if spdlog cannot be loaded try { const _spdlog = await import('spdlog'); - _spdlog.setAsyncMode(8192, 500); - return _spdlog.createRotatingLoggerAsync(name, logfilePath, filesize, filecount); + _spdlog.setFlushOn(LogLevel.Trace); + return _spdlog.createAsyncRotatingLogger(name, logfilePath, filesize, filecount); } catch (e) { console.error(e); } return null; } -export function createRotatingLogger(name: string, filename: string, filesize: number, filecount: number): spdlog.RotatingLogger { +export function createRotatingLogger(name: string, filename: string, filesize: number, filecount: number): Promise { const _spdlog: typeof spdlog = require.__$__nodeRequire('spdlog'); + _spdlog.setFlushOn(LogLevel.Trace); return _spdlog.createRotatingLogger(name, filename, filesize, filecount); } @@ -29,7 +30,7 @@ interface ILog { message: string; } -function log(logger: spdlog.RotatingLogger, level: LogLevel, message: string): void { +function log(logger: spdlog.Logger, level: LogLevel, message: string): void { switch (level) { case LogLevel.Trace: logger.trace(message); break; case LogLevel.Debug: logger.debug(message); break; @@ -45,7 +46,7 @@ export class SpdLogLogger extends AbstractMessageLogger implements ILogger { private buffer: ILog[] = []; private readonly _loggerCreationPromise: Promise; - private _logger: spdlog.RotatingLogger | undefined; + private _logger: spdlog.Logger | undefined; constructor( private readonly name: string, diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index c5d517cf81..28438cf180 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -19,7 +19,7 @@ import { IWindowsMainService, IWindowsCountChangedEvent, OpenContext } from 'vs/ import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; import { IMenubarData, IMenubarKeybinding, MenubarMenuItem, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, isMenubarMenuItemAction, IMenubarMenu, isMenubarMenuItemRecentAction, IMenubarMenuRecentItemAction } from 'vs/platform/menubar/common/menubar'; import { URI } from 'vs/base/common/uri'; -import { IStateService } from 'vs/platform/state/node/state'; +import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; @@ -70,7 +70,7 @@ export class Menubar { @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService, - @IStateService private readonly stateService: IStateService, + @IStateMainService private readonly stateMainService: IStateMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @ILogService private readonly logService: ILogService, @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, @@ -100,7 +100,7 @@ export class Menubar { } private restoreCachedMenubarData() { - const menubarData = this.stateService.getItem(Menubar.lastKnownMenubarStorageKey); + const menubarData = this.stateMainService.getItem(Menubar.lastKnownMenubarStorageKey); if (menubarData) { if (menubarData.menus) { this.menubarMenus = menubarData.menus; @@ -200,7 +200,7 @@ export class Menubar { this.keybindings = menubarData.keybindings; // Save off new menu and keybindings - this.stateService.setItem(Menubar.lastKnownMenubarStorageKey, menubarData); + this.stateMainService.setItem(Menubar.lastKnownMenubarStorageKey, menubarData); this.scheduleUpdateMenu(); } @@ -286,53 +286,60 @@ export class Menubar { } // File - const fileMenu = new Menu(); - const fileMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mFile', comment: ['&& denotes a mnemonic'] }, "&&File")), submenu: fileMenu }); - - this.setMenuById(fileMenu, 'File'); - menubar.append(fileMenuItem); + if (this.shouldDrawMenu('File')) { + const fileMenu = new Menu(); + const fileMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mFile', comment: ['&& denotes a mnemonic'] }, "&&File")), submenu: fileMenu }); + this.setMenuById(fileMenu, 'File'); + menubar.append(fileMenuItem); + } // Edit - const editMenu = new Menu(); - const editMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit")), submenu: editMenu }); - - this.setMenuById(editMenu, 'Edit'); - menubar.append(editMenuItem); + if (this.shouldDrawMenu('Edit')) { + const editMenu = new Menu(); + const editMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit")), submenu: editMenu }); + this.setMenuById(editMenu, 'Edit'); + menubar.append(editMenuItem); + } // Selection - /*const selectionMenu = new Menu(); - const selectionMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection")), submenu: selectionMenu }); - - this.setMenuById(selectionMenu, 'Selection'); - menubar.append(selectionMenuItem); {{SQL CARBON EDIT}} - Disable unused menus */ + /*if (this.shouldDrawMenu('Selection')) { + const selectionMenu = new Menu(); + const selectionMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection")), submenu: selectionMenu }); + this.setMenuById(selectionMenu, 'Selection'); + menubar.append(selectionMenuItem); + } {{SQL CARBON EDIT}} - Disable unused menus */ // View - const viewMenu = new Menu(); - const viewMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View")), submenu: viewMenu }); - - this.setMenuById(viewMenu, 'View'); - menubar.append(viewMenuItem); + if (this.shouldDrawMenu('View')) { + const viewMenu = new Menu(); + const viewMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View")), submenu: viewMenu }); + this.setMenuById(viewMenu, 'View'); + menubar.append(viewMenuItem); + } // Go - /* const gotoMenu = new Menu(); - const gotoMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Go")), submenu: gotoMenu }); - - this.setMenuById(gotoMenu, 'Go'); - menubar.append(gotoMenuItem); {{SQL CARBON EDIT}} - Disable unused menus */ + /* if (this.shouldDrawMenu('Go')) { + const gotoMenu = new Menu(); + const gotoMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Go")), submenu: gotoMenu }); + this.setMenuById(gotoMenu, 'Go'); + menubar.append(gotoMenuItem); + } {{SQL CARBON EDIT}} - Disable unused menus */ // Debug - /*const debugMenu = new Menu(); - const debugMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mRun', comment: ['&& denotes a mnemonic'] }, "&&Run")), submenu: debugMenu }); - - this.setMenuById(debugMenu, 'Run'); - menubar.append(debugMenuItem); {{SQL CARBON EDIT}} - Disable unused menus */ + /* if (this.shouldDrawMenu('Run')) { + const debugMenu = new Menu(); + const debugMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mRun', comment: ['&& denotes a mnemonic'] }, "&&Run")), submenu: debugMenu }); + this.setMenuById(debugMenu, 'Run'); + menubar.append(debugMenuItem); + } {{SQL CARBON EDIT}} - Disable unused menus */ // Terminal - /*const terminalMenu = new Menu(); - const terminalMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal")), submenu: terminalMenu }); - - this.setMenuById(terminalMenu, 'Terminal'); - menubar.append(terminalMenuItem); {{SQL CARBON EDIT}} - Disable unused menus */ + /* if (this.shouldDrawMenu('Terminal')) { + const terminalMenu = new Menu(); + const terminalMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal")), submenu: terminalMenu }); + this.setMenuById(terminalMenu, 'Terminal'); + menubar.append(terminalMenuItem); + } {{SQL CARBON EDIT}} - Disable unused menus */ // Mac: Window let macWindowMenuItem: MenuItem | undefined; @@ -347,11 +354,12 @@ export class Menubar { } // Help - const helpMenu = new Menu(); - const helpMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help")), submenu: helpMenu, role: 'help' }); - - this.setMenuById(helpMenu, 'Help'); - menubar.append(helpMenuItem); + if (this.shouldDrawMenu('Help')) { + const helpMenu = new Menu(); + const helpMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help")), submenu: helpMenu, role: 'help' }); + this.setMenuById(helpMenu, 'Help'); + menubar.append(helpMenuItem); + } if (menubar.items && menubar.items.length > 0) { Menu.setApplicationMenu(menubar); diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 67a28bd759..da5910c08f 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -5,7 +5,7 @@ import { Event } from 'vs/base/common/event'; import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, SaveDialogOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogReturnValue, MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes'; -import { IOpenedWindow, IWindowOpenable, IOpenEmptyWindowOptions, IOpenWindowOptions, IColorScheme } from 'vs/platform/windows/common/windows'; +import { IOpenedWindow, IWindowOpenable, IOpenEmptyWindowOptions, IOpenWindowOptions, IColorScheme, IPartsSplash } from 'vs/platform/windows/common/windows'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { URI } from 'vs/base/common/uri'; @@ -72,6 +72,8 @@ export interface ICommonNativeHostService { setMinimumSize(width: number | undefined, height: number | undefined): Promise; + saveWindowSplash(splash: IPartsSplash): Promise; + /** * Make the window focused. * @@ -97,7 +99,7 @@ export interface ICommonNativeHostService { setRepresentedFilename(path: string): Promise; setDocumentEdited(edited: boolean): Promise; openExternal(url: string): Promise; - moveItemToTrash(fullPath: string, deleteOnFail?: boolean): Promise; + moveItemToTrash(fullPath: string): Promise; isAdmin(): Promise; writeElevated(source: URI, target: URI, options?: { unlock?: boolean }): Promise; @@ -128,6 +130,10 @@ export interface ICommonNativeHostService { toggleWindowTabsBar(): Promise; updateTouchBar(items: ISerializableCommandAction[][]): Promise; + // macOS Shell command + installShellCommand(): Promise; + uninstallShellCommand(): Promise; + // Lifecycle notifyReady(): Promise relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): Promise; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 8305237209..17219e4332 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -3,11 +3,15 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { exec } from 'child_process'; +import { promisify } from 'util'; +import { localize } from 'vs/nls'; +import { realpath } from 'vs/base/node/extpath'; import { Emitter, Event } from 'vs/base/common/event'; 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, screen, Display } from 'electron'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; -import { IOpenedWindow, IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions, IColorScheme } from 'vs/platform/windows/common/windows'; +import { IOpenedWindow, IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions, IColorScheme, IPartsSplash } from 'vs/platform/windows/common/windows'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { isMacintosh, isWindows, isLinux, isLinuxSnap } from 'vs/base/common/platform'; import { ICommonNativeHostService, IOSProperties, IOSStatistics } from 'vs/platform/native/common/native'; @@ -15,7 +19,7 @@ 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/dialogMainService'; -import { SymlinkSupport } from 'vs/base/node/pfs'; +import { Promises, 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'; @@ -23,11 +27,12 @@ import { MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes'; import { arch, totalmem, release, platform, type, loadavg, freemem, cpus } from 'os'; import { virtualMachineHint } from 'vs/base/node/id'; import { ILogService } from 'vs/platform/log/common/log'; -import { dirname, join } from 'vs/base/common/path'; +import { dirname, join, resolve } from 'vs/base/common/path'; import { IProductService } from 'vs/platform/product/common/productService'; import { memoize } from 'vs/base/common/decorators'; import { Disposable } from 'vs/base/common/lifecycle'; import { ISharedProcess } from 'vs/platform/sharedProcess/node/sharedProcess'; +import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; export interface INativeHostMainService extends AddFirstParameterToFunctions /* only methods, not events */, number | undefined /* window ID */> { } @@ -50,7 +55,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService, - @IProductService private readonly productService: IProductService + @IProductService private readonly productService: IProductService, + @IThemeMainService private readonly themeMainService: IThemeMainService ) { super(); @@ -247,9 +253,106 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } } + async saveWindowSplash(windowId: number | undefined, splash: IPartsSplash): Promise { + this.themeMainService.saveWindowSplash(windowId, splash); + } + //#endregion + //#region macOS Shell Command + + async installShellCommand(windowId: number | undefined): Promise { + const { source, target } = await this.getShellCommandLink(); + + // Only install unless already existing + try { + const { symbolicLink } = await SymlinkSupport.stat(source); + if (symbolicLink && !symbolicLink.dangling) { + const linkTargetRealPath = await realpath(source); + if (target === linkTargetRealPath) { + return; + } + } + + // Different source, delete it first + await Promises.unlink(source); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; // throw on any error but file not found + } + } + + try { + await Promises.symlink(target, source); + } catch (error) { + if (error.code !== 'EACCES' && error.code !== 'ENOENT') { + throw error; + } + + const { response } = await this.showMessageBox(windowId, { + type: 'info', + message: localize('warnEscalation', "{0} will now prompt with 'osascript' for Administrator privileges to install the shell command.", this.productService.nameShort), + buttons: [localize('ok', "OK"), localize('cancel', "Cancel")], + cancelId: 1 + }); + + if (response === 0 /* OK */) { + try { + const command = `osascript -e "do shell script \\"mkdir -p /usr/local/bin && ln -sf \'${target}\' \'${source}\'\\" with administrator privileges"`; + await promisify(exec)(command); + } catch (error) { + throw new Error(localize('cantCreateBinFolder', "Unable to install the shell command '{0}'.", source)); + } + } + } + } + + async uninstallShellCommand(windowId: number | undefined): Promise { + const { source } = await this.getShellCommandLink(); + + try { + await Promises.unlink(source); + } catch (error) { + switch (error.code) { + case 'EACCES': + const { response } = await this.showMessageBox(windowId, { + type: 'info', + message: localize('warnEscalationUninstall', "{0} will now prompt with 'osascript' for Administrator privileges to uninstall the shell command.", this.productService.nameShort), + buttons: [localize('ok', "OK"), localize('cancel', "Cancel")], + cancelId: 1 + }); + + if (response === 0 /* OK */) { + try { + const command = `osascript -e "do shell script \\"rm \'${source}\'\\" with administrator privileges"`; + await promisify(exec)(command); + } catch (error) { + throw new Error(localize('cantUninstall', "Unable to uninstall the shell command '{0}'.", source)); + } + } + break; + case 'ENOENT': + break; // ignore file not found + default: + throw error; + } + } + } + + private async getShellCommandLink(): Promise<{ readonly source: string, readonly target: string }> { + const target = resolve(this.environmentMainService.appRoot, 'bin', 'code'); + const source = `/usr/local/bin/${this.productService.applicationName}`; + + // Ensure source exists + const sourceExists = await Promises.exists(target); + if (!sourceExists) { + throw new Error(localize('sourceMissing', "Unable to find shell script in '{0}'", target)); + } + + return { source, target }; + } + //#region Dialog async showMessageBox(windowId: number | undefined, options: MessageBoxOptions): Promise { @@ -376,8 +479,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain process.env['GDK_PIXBUF_MODULEDIR'] = gdkPixbufModuleDir; } - async moveItemToTrash(windowId: number | undefined, fullPath: string): Promise { - return shell.moveItemToTrash(fullPath); + moveItemToTrash(windowId: number | undefined, fullPath: string): Promise { + return shell.trashItem(fullPath); } async isAdmin(): Promise { @@ -604,9 +707,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain // Otherwise: normal quit else { - setTimeout(() => { - this.lifecycleMainService.quit(); - }, 10 /* delay to unwind callback stack (IPC) */); + this.lifecycleMainService.quit(); } } @@ -716,6 +817,27 @@ export class NativeHostMainService extends Disposable implements INativeHostMain async setPassword(windowId: number | undefined, service: string, account: string, password: string): Promise { const keytar = await this.withKeytar(); + const MAX_SET_ATTEMPTS = 3; + + // Sometimes Keytar has a problem talking to the keychain on the OS. To be more resilient, we retry a few times. + const setPasswordWithRetry = async (service: string, account: string, password: string) => { + let attempts = 0; + let error: any; + while (attempts < MAX_SET_ATTEMPTS) { + try { + await keytar.setPassword(service, account, password); + return; + } catch (e) { + error = e; + this.logService.warn('Error attempting to set a password: ', e); + attempts++; + await new Promise(resolve => setTimeout(resolve, 200)); + } + } + + // throw last error + throw error; + }; if (isWindows && password.length > NativeHostMainService.MAX_PASSWORD_LENGTH) { let index = 0; @@ -731,12 +853,12 @@ export class NativeHostMainService extends Disposable implements INativeHostMain hasNextChunk: hasNextChunk }; - await keytar.setPassword(service, chunk ? `${account}-${chunk}` : account, JSON.stringify(content)); + await setPasswordWithRetry(service, chunk ? `${account}-${chunk}` : account, JSON.stringify(content)); chunk++; } } else { - await keytar.setPassword(service, account, password); + await setPasswordWithRetry(service, account, password); } this._onDidChangePassword.fire({ service, account }); diff --git a/src/vs/platform/native/electron-sandbox/native.ts b/src/vs/platform/native/electron-sandbox/native.ts index 336eb82962..4d7a1c4f03 100644 --- a/src/vs/platform/native/electron-sandbox/native.ts +++ b/src/vs/platform/native/electron-sandbox/native.ts @@ -12,7 +12,7 @@ export const INativeHostService = createDecorator('nativeHos * A set of methods specific to a native host, i.e. unsupported in web * environments. * - * @see `IHostService` for methods that can be used in native and web + * @see {@link IHostService} for methods that can be used in native and web * hosts. */ export interface INativeHostService extends ICommonNativeHostService { } diff --git a/src/vs/platform/opener/browser/link.ts b/src/vs/platform/opener/browser/link.ts index ac5e086b8b..cc2ac7e2b0 100644 --- a/src/vs/platform/opener/browser/link.ts +++ b/src/vs/platform/opener/browser/link.ts @@ -6,11 +6,12 @@ import { Event } from 'vs/base/common/event'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { $, EventHelper, EventLike } from 'vs/base/browser/dom'; -import { domEvent } from 'vs/base/browser/event'; +import { DomEmitter, domEvent } from 'vs/base/browser/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; -import { Color } from 'vs/base/common/color'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; export interface ILinkDescriptor { readonly label: string; @@ -18,75 +19,86 @@ export interface ILinkDescriptor { readonly title?: string; } -export interface ILinkStyles { - readonly textLinkForeground?: Color; - readonly disabled?: boolean; +export interface ILinkOptions { + readonly opener?: (href: string) => void; + readonly textLinkForeground?: string; } export class Link extends Disposable { readonly el: HTMLAnchorElement; - private disabled: boolean; - private styles: ILinkStyles = { - textLinkForeground: Color.fromHex('#006AB1') - }; + private _enabled: boolean = true; + + get enabled(): boolean { + return this._enabled; + } + + set enabled(enabled: boolean) { + if (enabled) { + 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._enabled = false; + } else { + 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._enabled = true; + } + + this._enabled = enabled; + } constructor( link: ILinkDescriptor, + options: ILinkOptions | undefined = undefined, @IOpenerService openerService: IOpenerService ) { super(); - this.el = $('a', { + this.el = $('a.monaco-link', { tabIndex: 0, href: link.href, title: link.title }, link.label); - const onClick = domEvent(this.el, 'click'); + const onClickEmitter = this._register(new DomEmitter(this.el, 'click')); const onEnterPress = Event.chain(domEvent(this.el, 'keypress')) .map(e => new StandardKeyboardEvent(e)) .filter(e => e.keyCode === KeyCode.Enter) .event; - const onOpen = Event.any(onClick, onEnterPress); + const onOpen = Event.any(onClickEmitter.event, onEnterPress); this._register(onOpen(e => { + if (!this.enabled) { + return; + } + EventHelper.stop(e, true); - if (!this.disabled) { + + if (options?.opener) { + options.opener(link.href); + } else { openerService.open(link.href, { allowCommands: true }); } })); - this.disabled = false; - this.applyStyles(); - } - - style(styles: ILinkStyles): void { - this.styles = styles; - this.applyStyles(); - } - - private applyStyles(): void { - 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; - } - } + this.enabled = true; } } + +registerThemingParticipant((theme, collector) => { + const textLinkForegroundColor = theme.getColor(textLinkForeground); + if (textLinkForegroundColor) { + collector.addRule(`.monaco-link { color: ${textLinkForegroundColor}; }`); + } + + const textLinkActiveForegroundColor = theme.getColor(textLinkActiveForeground); + if (textLinkActiveForegroundColor) { + collector.addRule(`.monaco-link:hover { color: ${textLinkActiveForegroundColor}; }`); + } +}); diff --git a/src/vs/platform/opener/common/opener.ts b/src/vs/platform/opener/common/opener.ts index 8e05a7c20e..787319eb3b 100644 --- a/src/vs/platform/opener/common/opener.ts +++ b/src/vs/platform/opener/common/opener.ts @@ -109,6 +109,7 @@ export interface IOpenerService { /** * Resolve a resource to its external form. + * @throws whenever resolvers couldn't resolve this resource externally. */ resolveExternalUri(resource: URI, options?: ResolveExternalUriOptions): Promise; } diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index 850c2f2a64..6e593657a2 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -13,7 +13,7 @@ import { ISandboxConfiguration } from 'vs/base/parts/sandbox/common/sandboxTypes let product: IProductConfiguration; // Native sandbox environment -if (typeof globals.vscode !== 'undefined') { +if (typeof globals.vscode !== 'undefined' && typeof globals.vscode.context !== 'undefined') { const configuration: ISandboxConfiguration | undefined = globals.vscode.context.configuration(); if (configuration) { product = configuration.product; @@ -67,10 +67,10 @@ else { extensionAllowedProposedApi: [ 'ms-vscode.vscode-js-profile-flame', 'ms-vscode.vscode-js-profile-table', - 'ms-vscode.github-browser', - 'ms-vscode.github-richnav', 'ms-vscode.remotehub', - 'ms-vscode.remotehub-insiders' + 'ms-vscode.remotehub-insiders', + 'GitHub.remotehub', + 'GitHub.remotehub-insiders' ], }); } diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index 1028101880..bd5326c3c9 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -19,7 +19,7 @@ export interface IProgressService { readonly _serviceBrand: undefined; withProgress( - options: IProgressOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions, + options: IProgressOptions | IProgressDialogOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions, task: (progress: IProgress) => Promise, onDidCancel?: (choice?: number) => void ): Promise; @@ -66,6 +66,11 @@ export interface IProgressNotificationOptions extends IProgressOptions { readonly silent?: boolean; } +export interface IProgressDialogOptions extends IProgressOptions { + readonly delay?: number; + readonly detail?: string; +} + export interface IProgressWindowOptions extends IProgressOptions { readonly location: ProgressLocation.Window; readonly command?: string; @@ -134,7 +139,7 @@ export class UnmanagedProgress extends Disposable { private lastStep?: IProgressStep; constructor( - options: IProgressOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions, + options: IProgressOptions | IProgressDialogOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions, @IProgressService progressService: IProgressService, ) { super(); @@ -159,7 +164,6 @@ export class UnmanagedProgress extends Disposable { } } - export class LongRunningOperation extends Disposable { private currentOperationId = 0; private readonly currentOperationDisposables = this._register(new DisposableStore()); diff --git a/src/vs/platform/protocol/electron-main/protocol.ts b/src/vs/platform/protocol/electron-main/protocol.ts index e224973ab7..abf9292e36 100644 --- a/src/vs/platform/protocol/electron-main/protocol.ts +++ b/src/vs/platform/protocol/electron-main/protocol.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IDisposable } from 'vs/base/common/lifecycle'; @@ -34,12 +34,8 @@ export interface IProtocolMainService { /** * Allows to make an object accessible to a renderer * via `ipcRenderer.invoke(resource.toString())`. - * - * @param obj the (optional) object to make accessible to the - * renderer. Can be updated later via the `IObjectUrl#update` - * method too. */ - createIPCObjectUrl(obj?: T): IIPCObjectUrl; + createIPCObjectUrl(): IIPCObjectUrl; /** * Adds a `URI` as root to the list of allowed diff --git a/src/vs/platform/protocol/electron-main/protocolMainService.ts b/src/vs/platform/protocol/electron-main/protocolMainService.ts index a82ccda776..ad3a30f592 100644 --- a/src/vs/platform/protocol/electron-main/protocolMainService.ts +++ b/src/vs/platform/protocol/electron-main/protocolMainService.ts @@ -22,7 +22,7 @@ export class ProtocolMainService extends Disposable implements IProtocolMainServ declare readonly _serviceBrand: undefined; private readonly validRoots = TernarySearchTree.forUris(() => !isLinux); - private readonly validExtensions = new Set(['.png', '.jpg', '.jpeg', '.gif', '.bmp']); // https://github.com/microsoft/vscode/issues/119384 + private readonly validExtensions = new Set(['.svg', '.png', '.jpg', '.jpeg', '.gif', '.bmp']); // https://github.com/microsoft/vscode/issues/119384 constructor( @INativeEnvironmentService environmentService: INativeEnvironmentService, @@ -47,10 +47,10 @@ export class ProtocolMainService extends Disposable implements IProtocolMainServ const { defaultSession } = session; // Register vscode-file:// handler - defaultSession.protocol.registerFileProtocol(Schemas.vscodeFileResource, (request, callback) => this.handleResourceRequest(request, callback as unknown as ProtocolCallback)); + defaultSession.protocol.registerFileProtocol(Schemas.vscodeFileResource, (request, callback) => this.handleResourceRequest(request, callback)); // Intercept any file:// access - defaultSession.protocol.interceptFileProtocol(Schemas.file, (request, callback) => this.handleFileRequest(request, callback as unknown as ProtocolCallback)); + defaultSession.protocol.interceptFileProtocol(Schemas.file, (request, callback) => this.handleFileRequest(request, callback)); // Cleanup this._register(toDisposable(() => { @@ -142,7 +142,8 @@ export class ProtocolMainService extends Disposable implements IProtocolMainServ //#region IPC Object URLs - createIPCObjectUrl(obj: T): IIPCObjectUrl { + createIPCObjectUrl(): IIPCObjectUrl { + let obj: T | undefined = undefined; // Create unique URI const resource = URI.from({ @@ -152,7 +153,7 @@ export class ProtocolMainService extends Disposable implements IProtocolMainServ // Install IPC handler const channel = resource.toString(); - const handler = async (): Promise => obj; + const handler = async (): Promise => obj; ipcMain.handle(channel, handler); this.logService.trace(`IPC Object URL: Registered new channel ${channel}.`); diff --git a/src/vs/platform/quickinput/browser/commandsQuickAccess.ts b/src/vs/platform/quickinput/browser/commandsQuickAccess.ts index 502c6bfab6..d5f3ad8d25 100644 --- a/src/vs/platform/quickinput/browser/commandsQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/commandsQuickAccess.ts @@ -19,7 +19,8 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { isPromiseCanceledError } from 'vs/base/common/errors'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import Severity from 'vs/base/common/severity'; import { toErrorMessage } from 'vs/base/common/errorMessage'; export interface ICommandQuickPick extends IPickerQuickAccessItem { @@ -47,14 +48,14 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc @IKeybindingService private readonly keybindingService: IKeybindingService, @ICommandService private readonly commandService: ICommandService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @INotificationService private readonly notificationService: INotificationService + @IDialogService private readonly dialogService: IDialogService ) { super(AbstractCommandsQuickAccessProvider.PREFIX, options); this.options = options; } - protected async getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise> { + protected async _getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise> { // Ask subclass for all command picks const allCommandPicks = await this.getCommandPicks(disposables, token); @@ -162,7 +163,7 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc await this.commandService.executeCommand(commandPick.commandId); } catch (error) { if (!isPromiseCanceledError(error)) { - this.notificationService.error(localize('canNotRun', "Command '{0}' resulted in an error ({1})", commandPick.label, toErrorMessage(error))); + this.dialogService.show(Severity.Error, localize('canNotRun', "Command '{0}' resulted in an error ({1})", commandPick.label, toErrorMessage(error))); } } } diff --git a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts index 6510a77dfc..5d04410ade 100644 --- a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts @@ -5,7 +5,7 @@ import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { IQuickPickSeparator, IKeyMods, IQuickPickAcceptEvent } from 'vs/base/parts/quickinput/common/quickInput'; +import { IQuickPickSeparator, IKeyMods, IQuickPickDidAcceptEvent } from 'vs/base/parts/quickinput/common/quickInput'; import { IQuickAccessProvider } from 'vs/platform/quickinput/common/quickAccess'; import { IDisposable, DisposableStore, Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { timeout, isThenable } from 'vs/base/common/async'; @@ -42,7 +42,7 @@ export interface IPickerQuickAccessItem extends IQuickPickItem { * @param keyMods the state of modifier keys when the item was accepted. * @param event the underlying event that caused the accept to trigger. */ - accept?(keyMods: IKeyMods, event: IQuickPickAcceptEvent): void; + accept?(keyMods: IKeyMods, event: IQuickPickDidAcceptEvent): void; /** * A method that will be executed when a button of the pick item was @@ -122,7 +122,7 @@ export abstract class PickerQuickAccessProvider, skipEmpty?: boolean): boolean => { let items: readonly Pick[]; @@ -330,5 +330,5 @@ export abstract class PickerQuickAccessProvider | Promise> | FastAndSlowPicks | null; + protected abstract _getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Picks | Promise> | FastAndSlowPicks | null; } diff --git a/src/vs/platform/quickinput/browser/quickAccess.ts b/src/vs/platform/quickinput/browser/quickAccess.ts index 9196a7212c..c94ef9c6d9 100644 --- a/src/vs/platform/quickinput/browser/quickAccess.ts +++ b/src/vs/platform/quickinput/browser/quickAccess.ts @@ -31,7 +31,17 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon super(); } + pick(value = '', options?: IQuickAccessOptions): Promise { + return this.doShowOrPick(value, true, options); + } + show(value = '', options?: IQuickAccessOptions): void { + this.doShowOrPick(value, false, options); + } + + private doShowOrPick(value: string, pick: true, options?: IQuickAccessOptions): Promise; + private doShowOrPick(value: string, pick: false, options?: IQuickAccessOptions): void; + private doShowOrPick(value: string, pick: boolean, options?: IQuickAccessOptions): Promise | void { // Find provider for the value to show const [provider, descriptor] = this.getOrInstantiateProvider(value); @@ -99,6 +109,18 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon picker.ariaLabel = descriptor?.placeholder; } + // Pick mode: setup a promise that can be resolved + // with the selected items and prevent execution + let pickPromise: Promise | undefined = undefined; + let pickResolve: Function | undefined = undefined; + if (pick) { + pickPromise = new Promise(resolve => pickResolve = resolve); + disposables.add(once(picker.onWillAccept)(e => { + e.veto(); + picker.hide(); + })); + } + // Register listeners disposables.add(this.registerPickerListeners(picker, provider, descriptor, value)); @@ -119,12 +141,20 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon // Start to dispose once picker hides disposables.dispose(); + + // Resolve pick promise with selected items + pickResolve?.(picker.selectedItems); }); // Finally, show the picker. This is important because a provider // may not call this and then our disposables would leak that rely // on the onDidHide event. picker.show(); + + // Pick mode: return with promise + if (pick) { + return pickPromise; + } } private adjustValueSelection(picker: IQuickPick, descriptor?: IQuickAccessProviderDescriptor, options?: IQuickAccessOptions): void { diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 4a3dc8fd36..024bdc82b4 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -7,7 +7,7 @@ import { IQuickInputService, IQuickPickItem, IPickOptions, IInputOptions, IQuick import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; -import { inputBackground, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorForeground, inputValidationErrorBorder, badgeBackground, badgeForeground, contrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, progressBarBackground, widgetShadow, listFocusForeground, activeContrastBorder, pickerGroupBorder, pickerGroupForeground, quickInputForeground, quickInputBackground, quickInputTitleBackground, quickInputListFocusBackground, keybindingLabelBackground, keybindingLabelForeground, keybindingLabelBorder, keybindingLabelBottomBorder } from 'vs/platform/theme/common/colorRegistry'; +import { inputBackground, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorForeground, inputValidationErrorBorder, badgeBackground, badgeForeground, contrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, progressBarBackground, widgetShadow, activeContrastBorder, pickerGroupBorder, pickerGroupForeground, quickInputForeground, quickInputBackground, quickInputTitleBackground, quickInputListFocusBackground, keybindingLabelBackground, keybindingLabelForeground, keybindingLabelBorder, keybindingLabelBottomBorder, quickInputListFocusForeground } from 'vs/platform/theme/common/colorRegistry'; import { CancellationToken } from 'vs/base/common/cancellation'; import { computeStyles } from 'vs/platform/theme/common/styler'; import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -219,7 +219,7 @@ export class QuickInputService extends Themable implements IQuickInputService { list: computeStyles(this.theme, { listBackground: quickInputBackground, // Look like focused when inactive. - listInactiveFocusForeground: listFocusForeground, + listInactiveFocusForeground: quickInputListFocusForeground, listInactiveFocusBackground: quickInputListFocusBackground, listFocusOutline: activeContrastBorder, listInactiveFocusOutline: activeContrastBorder, diff --git a/src/vs/platform/quickinput/common/quickAccess.ts b/src/vs/platform/quickinput/common/quickAccess.ts index d6f0051814..1a803aad58 100644 --- a/src/vs/platform/quickinput/common/quickAccess.ts +++ b/src/vs/platform/quickinput/common/quickAccess.ts @@ -36,6 +36,13 @@ export interface IQuickAccessController { * Open the quick access picker with the optional value prefilled. */ show(value?: string, options?: IQuickAccessOptions): void; + + /** + * Same as `show()` but instead of executing the selected pick item, + * it will be returned. May return `undefined` in case no item was + * picked by the user. + */ + pick(value?: string, options?: IQuickAccessOptions): Promise; } export enum DefaultQuickAccessFilterValue { diff --git a/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts b/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts index 01cc74c32a..05ded1696f 100644 --- a/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts +++ b/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts @@ -40,6 +40,10 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot return this._cache.get(authority)!; } + async getCanonicalURI(uri: URI): Promise { + return uri; + } + getConnectionData(authority: string): IRemoteConnectionData | null { if (!this._cache.has(authority)) { return null; @@ -76,4 +80,7 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot RemoteAuthorities.setConnectionToken(authority, connectionToken); this._onDidChangeConnectionData.fire(); } + + _setCanonicalURIProvider(provider: (uri: URI) => Promise): void { + } } diff --git a/src/vs/platform/remote/common/remoteAuthorityResolver.ts b/src/vs/platform/remote/common/remoteAuthorityResolver.ts index a82728dc7e..39d866982a 100644 --- a/src/vs/platform/remote/common/remoteAuthorityResolver.ts +++ b/src/vs/platform/remote/common/remoteAuthorityResolver.ts @@ -5,6 +5,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; export const IRemoteAuthorityResolverService = createDecorator('remoteAuthorityResolverService'); @@ -15,15 +16,9 @@ export interface ResolvedAuthority { readonly connectionToken: string | undefined; } -export enum RemoteTrustOption { - Unknown = 0, - DisableTrust = 1, - MachineTrusted = 2 -} - export interface ResolvedOptions { readonly extensionHostEnv?: { [key: string]: string | null }; - readonly trust?: RemoteTrustOption; + readonly isTrusted?: boolean; } export interface TunnelDescription { @@ -98,9 +93,18 @@ export interface IRemoteAuthorityResolverService { resolveAuthority(authority: string): Promise; getConnectionData(authority: string): IRemoteConnectionData | null; + /** + * Get the canonical URI for a `vscode-remote://` URI. + * + * **NOTE**: This can throw e.g. in cases where there is no resolver installed for the specific remote authority. + * + * @param uri The `vscode-remote://` URI + */ + getCanonicalURI(uri: URI): Promise; _clearResolvedAuthority(authority: string): void; _setResolvedAuthority(resolvedAuthority: ResolvedAuthority, resolvedOptions?: ResolvedOptions): void; _setResolvedAuthorityError(authority: string, err: any): void; _setAuthorityConnectionToken(authority: string, connectionToken: string): void; + _setCanonicalURIProvider(provider: (uri: URI) => Promise): void; } diff --git a/src/vs/platform/remote/common/remoteHosts.ts b/src/vs/platform/remote/common/remoteHosts.ts index d795d282af..4e530f51ff 100644 --- a/src/vs/platform/remote/common/remoteHosts.ts +++ b/src/vs/platform/remote/common/remoteHosts.ts @@ -26,7 +26,7 @@ export function getRemoteName(authority: string | undefined): string | undefined return authority.substr(0, pos); } -function isVirtualResource(resource: URI) { +export function isVirtualResource(resource: URI) { return resource.scheme !== Schemas.file && resource.scheme !== Schemas.vscodeRemote; } @@ -42,3 +42,7 @@ export function getVirtualWorkspaceLocation(workspace: IWorkspace): { scheme: st export function getVirtualWorkspaceScheme(workspace: IWorkspace): string | undefined { return getVirtualWorkspaceLocation(workspace)?.scheme; } + +export function isVirtualWorkspace(workspace: IWorkspace): boolean { + return getVirtualWorkspaceLocation(workspace) !== undefined; +} diff --git a/src/vs/platform/remote/common/tunnel.ts b/src/vs/platform/remote/common/tunnel.ts index aaa9ac5bd9..3cd17161e2 100644 --- a/src/vs/platform/remote/common/tunnel.ts +++ b/src/vs/platform/remote/common/tunnel.ts @@ -86,6 +86,7 @@ export interface ITunnelService { readonly onTunnelOpened: Event; readonly onTunnelClosed: Event<{ host: string, port: number; }>; readonly canElevate: boolean; + readonly hasTunnelProvider: boolean; canTunnel(uri: URI): boolean; openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded?: boolean, isPublic?: boolean): Promise | undefined; @@ -141,6 +142,10 @@ export abstract class AbstractTunnelService implements ITunnelService { @ILogService protected readonly logService: ILogService ) { } + get hasTunnelProvider(): boolean { + return !!this._tunnelProvider; + } + setTunnelProvider(provider: ITunnelProvider | undefined, features: TunnelProviderFeatures): IDisposable { this._tunnelProvider = provider; if (!provider) { diff --git a/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts b/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts index df81a3c97a..39445255cf 100644 --- a/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts +++ b/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts @@ -8,22 +8,27 @@ import * as errors from 'vs/base/common/errors'; import { RemoteAuthorities } from 'vs/base/common/network'; import { Disposable } from 'vs/base/common/lifecycle'; import { Emitter } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; -class PendingResolveAuthorityRequest { +class PendingPromise { + public readonly promise: Promise; + public readonly input: I; + public result: R | null; + private _resolve!: (value: R) => void; + private _reject!: (err: any) => void; - public value: ResolverResult | null; - - constructor( - private readonly _resolve: (value: ResolverResult) => void, - private readonly _reject: (err: any) => void, - public readonly promise: Promise, - ) { - this.value = null; + constructor(request: I) { + this.input = request; + this.promise = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }); + this.result = null; } - resolve(value: ResolverResult): void { - this.value = value; - this._resolve(this.value); + resolve(result: R): void { + this.result = result; + this._resolve(this.result); } reject(err: any): void { @@ -38,40 +43,50 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot private readonly _onDidChangeConnectionData = this._register(new Emitter()); public readonly onDidChangeConnectionData = this._onDidChangeConnectionData.event; - private readonly _resolveAuthorityRequests: Map; + private readonly _resolveAuthorityRequests: Map>; private readonly _connectionTokens: Map; + private readonly _canonicalURIRequests: Map>; + private _canonicalURIProvider: ((uri: URI) => Promise) | null; constructor() { super(); - this._resolveAuthorityRequests = new Map(); + this._resolveAuthorityRequests = new Map>(); this._connectionTokens = new Map(); + this._canonicalURIRequests = new Map>(); + this._canonicalURIProvider = null; } resolveAuthority(authority: string): Promise { if (!this._resolveAuthorityRequests.has(authority)) { - let resolve: (value: ResolverResult) => void; - let reject: (err: any) => void; - const promise = new Promise((_resolve, _reject) => { - resolve = _resolve; - reject = _reject; - }); - this._resolveAuthorityRequests.set(authority, new PendingResolveAuthorityRequest(resolve!, reject!, promise)); + this._resolveAuthorityRequests.set(authority, new PendingPromise(authority)); } return this._resolveAuthorityRequests.get(authority)!.promise; } + async getCanonicalURI(uri: URI): Promise { + const key = uri.toString(); + if (!this._canonicalURIRequests.has(key)) { + const request = new PendingPromise(uri); + if (this._canonicalURIProvider) { + this._canonicalURIProvider(request.input).then((uri) => request.resolve(uri), (err) => request.reject(err)); + } + this._canonicalURIRequests.set(key, request); + } + return this._canonicalURIRequests.get(key)!.promise; + } + getConnectionData(authority: string): IRemoteConnectionData | null { if (!this._resolveAuthorityRequests.has(authority)) { return null; } const request = this._resolveAuthorityRequests.get(authority)!; - if (!request.value) { + if (!request.result) { return null; } const connectionToken = this._connectionTokens.get(authority); return { - host: request.value.authority.host, - port: request.value.authority.port, + host: request.result.authority.host, + port: request.result.authority.port, connectionToken: connectionToken }; } @@ -107,4 +122,11 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot RemoteAuthorities.setConnectionToken(authority, connectionToken); this._onDidChangeConnectionData.fire(); } + + _setCanonicalURIProvider(provider: (uri: URI) => Promise): void { + this._canonicalURIProvider = provider; + this._canonicalURIRequests.forEach((value) => { + this._canonicalURIProvider!(value.input).then((uri) => value.resolve(uri), (err) => value.reject(err)); + }); + } } diff --git a/src/vs/platform/remote/node/tunnelService.ts b/src/vs/platform/remote/node/tunnelService.ts index 73d740d108..5116f141b4 100644 --- a/src/vs/platform/remote/node/tunnelService.ts +++ b/src/vs/platform/remote/node/tunnelService.ts @@ -8,6 +8,7 @@ import { Barrier } from 'vs/base/common/async'; import { Disposable } from 'vs/base/common/lifecycle'; import { findFreePortFaster } from 'vs/base/node/ports'; import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { connectRemoteAgentTunnel, IConnectionOptions, IAddressProvider, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; @@ -15,8 +16,8 @@ import { AbstractTunnelService, RemoteTunnel } from 'vs/platform/remote/common/t import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory'; import { ISignService } from 'vs/platform/sign/common/sign'; -async function createRemoteTunnel(options: IConnectionOptions, tunnelRemoteHost: string, tunnelRemotePort: number, tunnelLocalPort?: number): Promise { - const tunnel = new NodeRemoteTunnel(options, tunnelRemoteHost, tunnelRemotePort, tunnelLocalPort); +async function createRemoteTunnel(options: IConnectionOptions, defaultTunnelHost: string, tunnelRemoteHost: string, tunnelRemotePort: number, tunnelLocalPort?: number): Promise { + const tunnel = new NodeRemoteTunnel(options, defaultTunnelHost, tunnelRemoteHost, tunnelRemotePort, tunnelLocalPort); return tunnel.waitForReady(); } @@ -38,7 +39,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { private readonly _socketsDispose: Map void> = new Map(); - constructor(options: IConnectionOptions, tunnelRemoteHost: string, tunnelRemotePort: number, private readonly suggestedLocalPort?: number) { + constructor(options: IConnectionOptions, private readonly defaultTunnelHost: string, tunnelRemoteHost: string, tunnelRemotePort: number, private readonly suggestedLocalPort?: number) { super(); this._options = options; this._server = net.createServer(); @@ -76,17 +77,19 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { // if that fails, the method above returns 0, which works out fine below... let address: string | net.AddressInfo | null = null; - address = (this._server.listen(localPort).address()); + this._server.listen(localPort, this.defaultTunnelHost); + await this._barrier.wait(); + address = this._server.address(); // It is possible for findFreePortFaster to return a port that there is already a server listening on. This causes the previous listen call to error out. if (!address) { localPort = 0; - address = (this._server.listen(localPort).address()); + this._server.listen(localPort, this.defaultTunnelHost); + await this._barrier.wait(); + address = this._server.address(); } this.tunnelLocalPort = address.port; - - await this._barrier.wait(); this.localAddress = `${this.tunnelRemoteHost === '127.0.0.1' ? '127.0.0.1' : 'localhost'}:${address.port}`; return this; } @@ -135,11 +138,16 @@ export class BaseTunnelService extends AbstractTunnelService { private readonly socketFactory: ISocketFactory, @ILogService logService: ILogService, @ISignService private readonly signService: ISignService, - @IProductService private readonly productService: IProductService + @IProductService private readonly productService: IProductService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(logService); } + private get defaultTunnelHost(): string { + return (this.configurationService.getValue('remote.localPortHost') === 'localhost') ? '127.0.0.1' : '0.0.0.0'; + } + 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) { @@ -160,7 +168,7 @@ export class BaseTunnelService extends AbstractTunnelService { ipcLogger: null }; - const tunnel = createRemoteTunnel(options, remoteHost, remotePort, localPort); + const tunnel = createRemoteTunnel(options, this.defaultTunnelHost, remoteHost, remotePort, localPort); this.logService.trace('ForwardedPorts: (TunnelService) Tunnel created without provider.'); this.addTunnelToMap(remoteHost, remotePort, tunnel); return tunnel; @@ -172,8 +180,9 @@ export class TunnelService extends BaseTunnelService { public constructor( @ILogService logService: ILogService, @ISignService signService: ISignService, - @IProductService productService: IProductService + @IProductService productService: IProductService, + @IConfigurationService configurationService: IConfigurationService ) { - super(nodeSocketFactory, logService, signService, productService); + super(nodeSocketFactory, logService, signService, productService, configurationService); } } diff --git a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts index 774cd758fd..ae61453b5e 100644 --- a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import product from 'vs/platform/product/common/product'; -import { BrowserWindow, ipcMain, Event as ElectronEvent, MessagePortMain, IpcMainEvent, RenderProcessGoneDetails } from 'electron'; +import { BrowserWindow, ipcMain, Event as ElectronEvent, MessagePortMain, IpcMainEvent } from 'electron'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { Barrier } from 'vs/base/common/async'; import { ILogService } from 'vs/platform/log/common/log'; @@ -18,7 +18,6 @@ import { connect as connectMessagePort } from 'vs/base/parts/ipc/electron-main/i import { assertIsDefined } from 'vs/base/common/types'; import { Emitter, Event } from 'vs/base/common/event'; import { WindowError } from 'vs/platform/windows/electron-main/windows'; -import { resolveShellEnv } from 'vs/platform/environment/node/shellEnv'; import { IProtocolMainService } from 'vs/platform/protocol/electron-main/protocol'; export class SharedProcess extends Disposable implements ISharedProcess { @@ -28,7 +27,7 @@ export class SharedProcess extends Disposable implements ISharedProcess { private window: BrowserWindow | undefined = undefined; private windowCloseListener: ((event: ElectronEvent) => void) | undefined = undefined; - private readonly _onDidError = this._register(new Emitter<{ type: WindowError, details: string | RenderProcessGoneDetails }>()); + private readonly _onDidError = this._register(new Emitter<{ type: WindowError, details?: { reason: string, exitCode: number } }>()); readonly onDidError = Event.buffer(this._onDidError.event); // buffer until we have a listener! constructor( @@ -139,9 +138,6 @@ export class SharedProcess extends Disposable implements ISharedProcess { // Always wait for first window asking for connection await this.firstWindowConnectionBarrier.wait(); - // Resolve shell environment - this.userEnv = { ...this.userEnv, ...(await resolveShellEnv(this.logService, this.environmentMainService.args, process.env)) }; - // Create window for shared process this.createWindow(); @@ -174,7 +170,6 @@ export class SharedProcess extends Disposable implements ISharedProcess { nodeIntegration: true, contextIsolation: false, enableWebSQL: false, - enableRemoteModule: false, spellcheck: false, nativeWindowOpen: true, images: false, @@ -188,7 +183,7 @@ export class SharedProcess extends Disposable implements ISharedProcess { machineId: this.machineId, windowId: this.window.id, appRoot: this.environmentMainService.appRoot, - nodeCachedDataDir: this.environmentMainService.nodeCachedDataDir, + codeCachePath: this.environmentMainService.codeCachePath, backupWorkspacesPath: this.environmentMainService.backupWorkspacesPath, userEnv: this.userEnv, args: this.environmentMainService.args, @@ -224,8 +219,8 @@ export class SharedProcess extends Disposable implements ISharedProcess { // We use `onUnexpectedError` explicitly because the error handler // will send the error to the active window to log in devtools too this.window.webContents.on('render-process-gone', (event, details) => this._onDidError.fire({ type: WindowError.CRASHED, details })); - this.window.on('unresponsive', () => this._onDidError.fire({ type: WindowError.UNRESPONSIVE, details: 'SharedProcess: detected unresponsive window' })); - this.window.webContents.on('did-fail-load', (event, errorCode, errorDescription) => this._onDidError.fire({ type: WindowError.LOAD, details: `SharedProcess: failed to load: ${errorDescription}` })); + this.window.on('unresponsive', () => this._onDidError.fire({ type: WindowError.UNRESPONSIVE })); + this.window.webContents.on('did-fail-load', (event, exitCode, reason) => this._onDidError.fire({ type: WindowError.LOAD, details: { reason, exitCode } })); } async connect(): Promise { diff --git a/src/vs/platform/state/node/state.ts b/src/vs/platform/state/electron-main/state.ts similarity index 72% rename from src/vs/platform/state/node/state.ts rename to src/vs/platform/state/electron-main/state.ts index 1310ab5cfd..84d53db355 100644 --- a/src/vs/platform/state/node/state.ts +++ b/src/vs/platform/state/electron-main/state.ts @@ -5,15 +5,19 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -export const IStateService = createDecorator('stateService'); +export const IStateMainService = createDecorator('stateMainService'); + +export interface IStateMainService { -export interface IStateService { readonly _serviceBrand: undefined; getItem(key: string, defaultValue: T): T; getItem(key: string, defaultValue?: T): T | undefined; setItem(key: string, data?: object | string | number | boolean | undefined | null): void; + setItems(items: readonly { key: string, data?: object | string | number | boolean | undefined | null }[]): void; removeItem(key: string): void; + + close(): Promise; } diff --git a/src/vs/platform/state/electron-main/stateMainService.ts b/src/vs/platform/state/electron-main/stateMainService.ts new file mode 100644 index 0000000000..fd0ffaede6 --- /dev/null +++ b/src/vs/platform/state/electron-main/stateMainService.ts @@ -0,0 +1,189 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { join } from 'vs/base/common/path'; +import { isUndefined, isUndefinedOrNull } from 'vs/base/common/types'; +import { IStateMainService } from 'vs/platform/state/electron-main/state'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; +import { ThrottledDelayer } from 'vs/base/common/async'; +import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { VSBuffer } from 'vs/base/common/buffer'; + +type StorageDatabase = { [key: string]: unknown; }; + +export class FileStorage { + + private storage: StorageDatabase = Object.create(null); + private lastSavedStorageContents = ''; + + private readonly flushDelayer = new ThrottledDelayer(100 /* buffer saves over a short time */); + + private initializing: Promise | undefined = undefined; + private closing: Promise | undefined = undefined; + + constructor( + private readonly storagePath: URI, + private readonly logService: ILogService, + private readonly fileService: IFileService + ) { + } + + init(): Promise { + if (!this.initializing) { + this.initializing = this.doInit(); + } + + return this.initializing; + } + + private async doInit(): Promise { + try { + this.lastSavedStorageContents = (await this.fileService.readFile(this.storagePath)).value.toString(); + this.storage = JSON.parse(this.lastSavedStorageContents); + } catch (error) { + if ((error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) { + this.logService.error(error); + } + } + } + + getItem(key: string, defaultValue: T): T; + getItem(key: string, defaultValue?: T): T | undefined; + getItem(key: string, defaultValue?: T): T | undefined { + const res = this.storage[key]; + if (isUndefinedOrNull(res)) { + return defaultValue; + } + + return res as T; + } + + setItem(key: string, data?: object | string | number | boolean | undefined | null): void { + this.setItems([{ key, data }]); + } + + setItems(items: readonly { key: string, data?: object | string | number | boolean | undefined | null }[]): void { + let save = false; + + for (const { key, data } of items) { + + // Shortcut for data that did not change + if (this.storage[key] === data) { + continue; + } + + // Remove items when they are undefined or null + if (isUndefinedOrNull(data)) { + if (!isUndefined(this.storage[key])) { + this.storage[key] = undefined; + save = true; + } + } + + // Otherwise add an item + else { + this.storage[key] = data; + save = true; + } + } + + if (save) { + this.save(); + } + } + + removeItem(key: string): void { + + // Only update if the key is actually present (not undefined) + if (!isUndefined(this.storage[key])) { + this.storage[key] = undefined; + this.save(); + } + } + + private async save(delay?: number): Promise { + if (this.closing) { + return; // already about to close + } + + return this.flushDelayer.trigger(() => this.doSave(), delay); + } + + private async doSave(): Promise { + if (!this.initializing) { + return; // if we never initialized, we should not save our state + } + + // Make sure to wait for init to finish first + await this.initializing; + + // Return early if the database has not changed + const serializedDatabase = JSON.stringify(this.storage, null, 4); + if (serializedDatabase === this.lastSavedStorageContents) { + return; + } + + // Write to disk + try { + await this.fileService.writeFile(this.storagePath, VSBuffer.fromString(serializedDatabase)); + this.lastSavedStorageContents = serializedDatabase; + } catch (error) { + this.logService.error(error); + } + } + + async close(): Promise { + if (!this.closing) { + this.closing = this.flushDelayer.trigger(() => this.doSave(), 0 /* as soon as possible */); + } + + return this.closing; + } +} + +export class StateMainService implements IStateMainService { + + declare readonly _serviceBrand: undefined; + + private static readonly STATE_FILE = 'storage.json'; + + private readonly fileStorage: FileStorage; + + constructor( + @IEnvironmentMainService environmentMainService: IEnvironmentMainService, + @ILogService logService: ILogService, + @IFileService fileService: IFileService + ) { + this.fileStorage = new FileStorage(URI.file(join(environmentMainService.userDataPath, StateMainService.STATE_FILE)), logService, fileService); + } + + async init(): Promise { + return this.fileStorage.init(); + } + + getItem(key: string, defaultValue: T): T; + getItem(key: string, defaultValue?: T): T | undefined; + getItem(key: string, defaultValue?: T): T | undefined { + return this.fileStorage.getItem(key, defaultValue); + } + + setItem(key: string, data?: object | string | number | boolean | undefined | null): void { + this.fileStorage.setItem(key, data); + } + + setItems(items: readonly { key: string, data?: object | string | number | boolean | undefined | null }[]): void { + this.fileStorage.setItems(items); + } + + removeItem(key: string): void { + this.fileStorage.removeItem(key); + } + + close(): Promise { + return this.fileStorage.close(); + } +} diff --git a/src/vs/platform/state/node/stateService.ts b/src/vs/platform/state/node/stateService.ts deleted file mode 100644 index c5e29ac11e..0000000000 --- a/src/vs/platform/state/node/stateService.ts +++ /dev/null @@ -1,158 +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 path from 'vs/base/common/path'; -import * as fs from 'fs'; -import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -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'; - -type StorageDatabase = { [key: string]: any; }; - -export class FileStorage { - - private _database: StorageDatabase | null = null; - private lastFlushedSerializedDatabase: string | null = null; - - constructor(private dbPath: string, private onError: (error: Error) => void) { } - - private get database(): StorageDatabase { - if (!this._database) { - this._database = this.loadSync(); - } - - return this._database; - } - - async init(): Promise { - if (this._database) { - return; // return if database was already loaded - } - - const database = await this.loadAsync(); - - if (this._database) { - return; // return if database was already loaded - } - - this._database = database; - } - - private loadSync(): StorageDatabase { - try { - this.lastFlushedSerializedDatabase = fs.readFileSync(this.dbPath).toString(); - - return JSON.parse(this.lastFlushedSerializedDatabase); - } catch (error) { - if (error.code !== 'ENOENT') { - this.onError(error); - } - - return {}; - } - } - - private async loadAsync(): Promise { - try { - this.lastFlushedSerializedDatabase = (await fs.promises.readFile(this.dbPath)).toString(); - - return JSON.parse(this.lastFlushedSerializedDatabase); - } catch (error) { - if (error.code !== 'ENOENT') { - this.onError(error); - } - - return {}; - } - } - - getItem(key: string, defaultValue: T): T; - getItem(key: string, defaultValue?: T): T | undefined; - getItem(key: string, defaultValue?: T): T | undefined { - const res = this.database[key]; - if (isUndefinedOrNull(res)) { - return defaultValue; - } - - return res; - } - - setItem(key: string, data?: object | string | number | boolean | undefined | null): void { - - // Remove an item when it is undefined or null - if (isUndefinedOrNull(data)) { - return this.removeItem(key); - } - - // Shortcut for primitives that did not change - if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean') { - if (this.database[key] === data) { - return; - } - } - - this.database[key] = data; - this.saveSync(); - } - - removeItem(key: string): void { - - // Only update if the key is actually present (not undefined) - if (!isUndefined(this.database[key])) { - this.database[key] = undefined; - this.saveSync(); - } - } - - private saveSync(): void { - const serializedDatabase = JSON.stringify(this.database, null, 4); - if (serializedDatabase === this.lastFlushedSerializedDatabase) { - return; // return early if the database has not changed - } - - try { - writeFileSync(this.dbPath, serializedDatabase); // permission issue can happen here - this.lastFlushedSerializedDatabase = serializedDatabase; - } catch (error) { - this.onError(error); - } - } -} - -export class StateService implements IStateService { - - declare readonly _serviceBrand: undefined; - - private static readonly STATE_FILE = 'storage.json'; - - private fileStorage: FileStorage; - - constructor( - @INativeEnvironmentService environmentService: INativeEnvironmentService, - @ILogService logService: ILogService - ) { - this.fileStorage = new FileStorage(path.join(environmentService.userDataPath, StateService.STATE_FILE), error => logService.error(error)); - } - - init(): Promise { - return this.fileStorage.init(); - } - - getItem(key: string, defaultValue: T): T; - getItem(key: string, defaultValue?: T): T | undefined; - getItem(key: string, defaultValue?: T): T | undefined { - return this.fileStorage.getItem(key, defaultValue); - } - - setItem(key: string, data?: object | string | number | boolean | undefined | null): void { - this.fileStorage.setItem(key, data); - } - - removeItem(key: string): void { - this.fileStorage.removeItem(key); - } -} diff --git a/src/vs/platform/state/test/electron-main/state.test.ts b/src/vs/platform/state/test/electron-main/state.test.ts new file mode 100644 index 0000000000..a998b2308d --- /dev/null +++ b/src/vs/platform/state/test/electron-main/state.test.ts @@ -0,0 +1,202 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { tmpdir } from 'os'; +import { readFileSync } from 'fs'; +import { join } from 'vs/base/common/path'; +import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { FileStorage } from 'vs/platform/state/electron-main/stateMainService'; +import { Promises, writeFileSync } from 'vs/base/node/pfs'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; +import { IFileService } from 'vs/platform/files/common/files'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; + +flakySuite('StateMainService', () => { + + let testDir: string; + let fileService: IFileService; + let logService: ILogService; + let diskFileSystemProvider: DiskFileSystemProvider; + + setup(() => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'statemainservice'); + + logService = new NullLogService(); + + fileService = new FileService(logService); + diskFileSystemProvider = new DiskFileSystemProvider(logService); + fileService.registerProvider(Schemas.file, diskFileSystemProvider); + + return Promises.mkdir(testDir, { recursive: true }); + }); + + teardown(() => { + fileService.dispose(); + diskFileSystemProvider.dispose(); + + return Promises.rm(testDir); + }); + + test('Basics', async function () { + const storageFile = join(testDir, 'storage.json'); + writeFileSync(storageFile, ''); + + let service = new FileStorage(URI.file(storageFile), logService, fileService); + await service.init(); + + service.setItem('some.key', 'some.value'); + assert.strictEqual(service.getItem('some.key'), 'some.value'); + + service.removeItem('some.key'); + assert.strictEqual(service.getItem('some.key', 'some.default'), 'some.default'); + + assert.ok(!service.getItem('some.unknonw.key')); + + service.setItem('some.other.key', 'some.other.value'); + + await service.close(); + + service = new FileStorage(URI.file(storageFile), logService, fileService); + await service.init(); + + assert.strictEqual(service.getItem('some.other.key'), 'some.other.value'); + + service.setItem('some.other.key', 'some.other.value'); + assert.strictEqual(service.getItem('some.other.key'), 'some.other.value'); + + service.setItem('some.undefined.key', undefined); + assert.strictEqual(service.getItem('some.undefined.key', 'some.default'), 'some.default'); + + service.setItem('some.null.key', null); + assert.strictEqual(service.getItem('some.null.key', 'some.default'), 'some.default'); + + service.setItems([ + { key: 'some.setItems.key1', data: 'some.value' }, + { key: 'some.setItems.key2', data: 0 }, + { key: 'some.setItems.key3', data: true }, + { key: 'some.setItems.key4', data: null }, + { key: 'some.setItems.key5', data: undefined } + ]); + + assert.strictEqual(service.getItem('some.setItems.key1'), 'some.value'); + assert.strictEqual(service.getItem('some.setItems.key2'), 0); + assert.strictEqual(service.getItem('some.setItems.key3'), true); + assert.strictEqual(service.getItem('some.setItems.key4'), undefined); + assert.strictEqual(service.getItem('some.setItems.key5'), undefined); + + service.setItems([ + { key: 'some.setItems.key1', data: undefined }, + { key: 'some.setItems.key2', data: undefined }, + { key: 'some.setItems.key3', data: undefined }, + { key: 'some.setItems.key4', data: null }, + { key: 'some.setItems.key5', data: undefined } + ]); + + assert.strictEqual(service.getItem('some.setItems.key1'), undefined); + assert.strictEqual(service.getItem('some.setItems.key2'), undefined); + assert.strictEqual(service.getItem('some.setItems.key3'), undefined); + assert.strictEqual(service.getItem('some.setItems.key4'), undefined); + assert.strictEqual(service.getItem('some.setItems.key5'), undefined); + }); + + test('Multiple ops are buffered and applied', async function () { + const storageFile = join(testDir, 'storage.json'); + writeFileSync(storageFile, ''); + + let service = new FileStorage(URI.file(storageFile), logService, fileService); + await service.init(); + + service.setItem('some.key1', 'some.value1'); + service.setItem('some.key2', 'some.value2'); + service.setItem('some.key3', 'some.value3'); + service.setItem('some.key4', 'some.value4'); + service.removeItem('some.key4'); + + assert.strictEqual(service.getItem('some.key1'), 'some.value1'); + assert.strictEqual(service.getItem('some.key2'), 'some.value2'); + assert.strictEqual(service.getItem('some.key3'), 'some.value3'); + assert.strictEqual(service.getItem('some.key4'), undefined); + + await service.close(); + + service = new FileStorage(URI.file(storageFile), logService, fileService); + await service.init(); + + assert.strictEqual(service.getItem('some.key1'), 'some.value1'); + assert.strictEqual(service.getItem('some.key2'), 'some.value2'); + assert.strictEqual(service.getItem('some.key3'), 'some.value3'); + assert.strictEqual(service.getItem('some.key4'), undefined); + }); + + test('Used before init', async function () { + const storageFile = join(testDir, 'storage.json'); + writeFileSync(storageFile, ''); + + let service = new FileStorage(URI.file(storageFile), logService, fileService); + + service.setItem('some.key1', 'some.value1'); + service.setItem('some.key2', 'some.value2'); + service.setItem('some.key3', 'some.value3'); + service.setItem('some.key4', 'some.value4'); + service.removeItem('some.key4'); + + assert.strictEqual(service.getItem('some.key1'), 'some.value1'); + assert.strictEqual(service.getItem('some.key2'), 'some.value2'); + assert.strictEqual(service.getItem('some.key3'), 'some.value3'); + assert.strictEqual(service.getItem('some.key4'), undefined); + + await service.init(); + + assert.strictEqual(service.getItem('some.key1'), 'some.value1'); + assert.strictEqual(service.getItem('some.key2'), 'some.value2'); + assert.strictEqual(service.getItem('some.key3'), 'some.value3'); + assert.strictEqual(service.getItem('some.key4'), undefined); + }); + + test('Used after close', async function () { + const storageFile = join(testDir, 'storage.json'); + writeFileSync(storageFile, ''); + + const service = new FileStorage(URI.file(storageFile), logService, fileService); + + await service.init(); + + service.setItem('some.key1', 'some.value1'); + service.setItem('some.key2', 'some.value2'); + service.setItem('some.key3', 'some.value3'); + service.setItem('some.key4', 'some.value4'); + + await service.close(); + + service.setItem('some.key5', 'some.marker'); + + const contents = readFileSync(storageFile).toString(); + assert.ok(contents.includes('some.value1')); + assert.ok(!contents.includes('some.marker')); + + await service.close(); + }); + + test('Closed before init', async function () { + const storageFile = join(testDir, 'storage.json'); + writeFileSync(storageFile, ''); + + const service = new FileStorage(URI.file(storageFile), logService, fileService); + + service.setItem('some.key1', 'some.value1'); + service.setItem('some.key2', 'some.value2'); + service.setItem('some.key3', 'some.value3'); + service.setItem('some.key4', 'some.value4'); + + await service.close(); + + const contents = readFileSync(storageFile).toString(); + assert.strictEqual(contents.length, 0); + }); +}); diff --git a/src/vs/platform/state/test/node/state.test.ts b/src/vs/platform/state/test/node/state.test.ts deleted file mode 100644 index fedba5a02b..0000000000 --- a/src/vs/platform/state/test/node/state.test.ts +++ /dev/null @@ -1,57 +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 { 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 { rimraf, writeFileSync } from 'vs/base/node/pfs'; - -flakySuite('StateService', () => { - - let testDir: string; - - setup(() => { - testDir = getRandomTestPath(tmpdir(), 'vsctests', 'stateservice'); - - return promises.mkdir(testDir, { recursive: true }); - }); - - 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.strictEqual(service.getItem('some.key'), 'some.value'); - - service.removeItem('some.key'); - assert.strictEqual(service.getItem('some.key', 'some.default'), 'some.default'); - - assert.ok(!service.getItem('some.unknonw.key')); - - service.setItem('some.other.key', 'some.other.value'); - - service = new FileStorage(storageFile, () => null); - - assert.strictEqual(service.getItem('some.other.key'), 'some.other.value'); - - service.setItem('some.other.key', 'some.other.value'); - assert.strictEqual(service.getItem('some.other.key'), 'some.other.value'); - - service.setItem('some.undefined.key', undefined); - assert.strictEqual(service.getItem('some.undefined.key', 'some.default'), 'some.default'); - - service.setItem('some.null.key', null); - assert.strictEqual(service.getItem('some.null.key', 'some.default'), 'some.default'); - }); -}); diff --git a/src/vs/platform/storage/browser/storageService.ts b/src/vs/platform/storage/browser/storageService.ts index 5d1859a5a9..a74278e1e9 100644 --- a/src/vs/platform/storage/browser/storageService.ts +++ b/src/vs/platform/storage/browser/storageService.ts @@ -3,17 +3,17 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; -import { Emitter } from 'vs/base/common/event'; -import { StorageScope, IS_NEW_KEY, AbstractStorageService } from 'vs/platform/storage/common/storage'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Event } from 'vs/base/common/event'; +import { StorageScope, IS_NEW_KEY, AbstractStorageService, StorageTarget } from 'vs/platform/storage/common/storage'; import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; -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 { IStorage, Storage, IStorageDatabase, IUpdateRequest, InMemoryStorageDatabase } from 'vs/base/parts/storage/common/storage'; import { Promises } from 'vs/base/common/async'; -import { VSBuffer } from 'vs/base/common/buffer'; +import { ILogService } from 'vs/platform/log/common/log'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; +import { joinPath } from 'vs/base/common/resources'; export class BrowserStorageService extends AbstractStorageService { @@ -22,40 +22,41 @@ export class BrowserStorageService extends AbstractStorageService { private globalStorage: IStorage | undefined; private workspaceStorage: IStorage | undefined; - private globalStorageDatabase: FileStorageDatabase | undefined; - private workspaceStorageDatabase: FileStorageDatabase | undefined; - - private globalStorageFile: URI | undefined; - private workspaceStorageFile: URI | undefined; + private globalStorageDatabase: IIndexedDBStorageDatabase | undefined; + private workspaceStorageDatabase: IIndexedDBStorageDatabase | undefined; get hasPendingUpdate(): boolean { - return (!!this.globalStorageDatabase && this.globalStorageDatabase.hasPendingUpdate) || (!!this.workspaceStorageDatabase && this.workspaceStorageDatabase.hasPendingUpdate); + return Boolean(this.globalStorageDatabase?.hasPendingUpdate || this.workspaceStorageDatabase?.hasPendingUpdate); } constructor( private readonly payload: IWorkspaceInitializationPayload, + @ILogService private readonly logService: ILogService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @IFileService private readonly fileService: IFileService ) { super({ flushInterval: BrowserStorageService.BROWSER_DEFAULT_FLUSH_INTERVAL }); } + private getId(scope: StorageScope): string { + return scope === StorageScope.GLOBAL ? 'global' : this.payload.id; + } + protected async doInitialize(): Promise { - // Ensure state folder exists - const stateRoot = joinPath(this.environmentService.userRoamingDataHome, 'state'); - await this.fileService.createFolder(stateRoot); + // Create Storage in Parallel + const [workspaceStorageDatabase, globalStorageDatabase] = await Promises.settled([ + IndexedDBStorageDatabase.create(this.getId(StorageScope.WORKSPACE), this.logService), + IndexedDBStorageDatabase.create(this.getId(StorageScope.GLOBAL), this.logService) + ]); // Workspace Storage - this.workspaceStorageFile = joinPath(stateRoot, `${this.payload.id}.json`); - - this.workspaceStorageDatabase = this._register(new FileStorageDatabase(this.workspaceStorageFile, false /* do not watch for external changes */, this.fileService)); + this.workspaceStorageDatabase = this._register(workspaceStorageDatabase); this.workspaceStorage = this._register(new Storage(this.workspaceStorageDatabase)); this._register(this.workspaceStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.WORKSPACE, key))); // Global Storage - this.globalStorageFile = joinPath(stateRoot, 'global.json'); - this.globalStorageDatabase = this._register(new FileStorageDatabase(this.globalStorageFile, true /* watch for external changes */, this.fileService)); + this.globalStorageDatabase = this._register(globalStorageDatabase); this.globalStorage = this._register(new Storage(this.globalStorageDatabase)); this._register(this.globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key))); @@ -68,6 +69,7 @@ export class BrowserStorageService extends AbstractStorageService { // Check to see if this is the first time we are "opening" the application const firstOpen = this.globalStorage.getBoolean(IS_NEW_KEY); if (firstOpen === undefined) { + await this.migrateOldStorage(StorageScope.GLOBAL); // TODO@bpasero remove browser storage migration this.globalStorage.set(IS_NEW_KEY, true); } else if (firstOpen) { this.globalStorage.set(IS_NEW_KEY, false); @@ -76,18 +78,49 @@ export class BrowserStorageService extends AbstractStorageService { // Check to see if this is the first time we are "opening" this workspace const firstWorkspaceOpen = this.workspaceStorage.getBoolean(IS_NEW_KEY); if (firstWorkspaceOpen === undefined) { + await this.migrateOldStorage(StorageScope.WORKSPACE); // TODO@bpasero remove browser storage migration this.workspaceStorage.set(IS_NEW_KEY, true); } else if (firstWorkspaceOpen) { this.workspaceStorage.set(IS_NEW_KEY, false); } } + private async migrateOldStorage(scope: StorageScope): Promise { + try { + const stateRoot = joinPath(this.environmentService.userRoamingDataHome, 'state'); + + if (scope === StorageScope.GLOBAL) { + const globalStorageFile = joinPath(stateRoot, 'global.json'); + const globalItemsRaw = await this.fileService.readFile(globalStorageFile); + const globalItems = new Map(JSON.parse(globalItemsRaw.value.toString())); + + for (const [key, value] of globalItems) { + this.globalStorage?.set(key, value); + } + + await this.fileService.del(globalStorageFile); + } else if (scope === StorageScope.WORKSPACE) { + const workspaceStorageFile = joinPath(stateRoot, `${this.payload.id}.json`); + const workspaceItemsRaw = await this.fileService.readFile(workspaceStorageFile); + const workspaceItems = new Map(JSON.parse(workspaceItemsRaw.value.toString())); + + for (const [key, value] of workspaceItems) { + this.workspaceStorage?.set(key, value); + } + + await this.fileService.del(workspaceStorageFile); + } + } catch (error) { + // ignore + } + } + protected getStorage(scope: StorageScope): IStorage | undefined { return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage; } protected getLogDetails(scope: StorageScope): string | undefined { - return scope === StorageScope.GLOBAL ? this.globalStorageFile?.toString() : this.workspaceStorageFile?.toString(); + return this.getId(scope); } async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise { @@ -118,144 +151,214 @@ export class BrowserStorageService extends AbstractStorageService { // get triggered in this phase. this.dispose(); } + + async clear(): Promise { + + // Clear key/values + for (const scope of [StorageScope.GLOBAL, StorageScope.WORKSPACE]) { + for (const target of [StorageTarget.USER, StorageTarget.MACHINE]) { + for (const key of this.keys(scope, target)) { + this.remove(key, scope); + } + } + + await this.getStorage(scope)?.whenFlushed(); + } + + // Clear databases + await Promises.settled([ + this.globalStorageDatabase?.clear() ?? Promise.resolve(), + this.workspaceStorageDatabase?.clear() ?? Promise.resolve() + ]); + } } -export class FileStorageDatabase extends Disposable implements IStorageDatabase { +interface IIndexedDBStorageDatabase extends IStorageDatabase, IDisposable { - private readonly _onDidChangeItemsExternal = this._register(new Emitter()); - readonly onDidChangeItemsExternal = this._onDidChangeItemsExternal.event; + /** + * Whether an update in the DB is currently pending + * (either update or delete operation). + */ + readonly hasPendingUpdate: boolean; - private cache: Map | undefined; + /** + * For testing only. + */ + clear(): Promise; +} - private pendingUpdate: Promise = Promise.resolve(); +class InMemoryIndexedDBStorageDatabase extends InMemoryStorageDatabase implements IIndexedDBStorageDatabase { - private _hasPendingUpdate = false; - get hasPendingUpdate(): boolean { - return this._hasPendingUpdate; + readonly hasPendingUpdate = false; + + async clear(): Promise { + (await this.getItems()).clear(); } - private isWatching = false; + dispose(): void { + // No-op + } +} - constructor( - private readonly file: URI, - private readonly watchForExternalChanges: boolean, - @IFileService private readonly fileService: IFileService +export class IndexedDBStorageDatabase extends Disposable implements IIndexedDBStorageDatabase { + + static async create(id: string, logService: ILogService): Promise { + try { + const database = new IndexedDBStorageDatabase(id, logService); + await database.whenConnected; + + return database; + } catch (error) { + logService.error(`[IndexedDB Storage ${id}] create(): ${toErrorMessage(error, true)}`); + + return new InMemoryIndexedDBStorageDatabase(); + } + } + + private static readonly STORAGE_DATABASE_PREFIX = 'vscode-web-state-db-'; + private static readonly STORAGE_OBJECT_STORE = 'ItemTable'; + + readonly onDidChangeItemsExternal = Event.None; // IndexedDB currently does not support observers (https://github.com/w3c/IndexedDB/issues/51) + + private pendingUpdate: Promise | undefined = undefined; + get hasPendingUpdate(): boolean { return !!this.pendingUpdate; } + + private readonly name: string; + private readonly whenConnected: Promise; + + private constructor( + id: string, + private readonly logService: ILogService ) { super(); + + this.name = `${IndexedDBStorageDatabase.STORAGE_DATABASE_PREFIX}${id}`; + this.whenConnected = this.connect(); } - private async ensureWatching(): Promise { - if (this.isWatching || !this.watchForExternalChanges) { - return; - } + private connect(): Promise { + return new Promise((resolve, reject) => { + const request = window.indexedDB.open(this.name); - const exists = await this.fileService.exists(this.file); - if (this.isWatching || !exists) { - return; // file must exist to be watched - } + // Create `ItemTable` object-store when this DB is new + request.onupgradeneeded = () => { + request.result.createObjectStore(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE); + }; - this.isWatching = true; + // IndexedDB opened successfully + request.onsuccess = () => resolve(request.result); - this._register(this.fileService.watch(this.file)); - this._register(this.fileService.onDidFilesChange(e => { - if (document.hasFocus()) { - return; // optimization: ignore changes from ourselves by checking for focus - } - - if (!e.contains(this.file, FileChangeType.UPDATED)) { - return; // not our file - } - - this.onDidStorageChangeExternal(); - })); + // Fail on error (we will then fallback to in-memory DB) + request.onerror = () => reject(request.error); + }); } - private async onDidStorageChangeExternal(): Promise { - const items = await this.doGetItemsFromFile(); + getItems(): Promise> { + return new Promise>(async resolve => { + const items = new Map(); - // pervious cache, diff for changes - let changed = new Map(); - let deleted = new Set(); - if (this.cache) { - items.forEach((value, key) => { - const existingValue = this.cache?.get(key); - if (existingValue !== value) { - changed.set(key, value); + // Open a IndexedDB Cursor to iterate over key/values + const db = await this.whenConnected; + const transaction = db.transaction(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE, 'readonly'); + const objectStore = transaction.objectStore(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE); + const cursor = objectStore.openCursor(); + if (!cursor) { + return resolve(items); // this means the `ItemTable` was empty + } + + // Iterate over rows of `ItemTable` until the end + cursor.onsuccess = () => { + if (cursor.result) { + + // Keep cursor key/value in our map + if (typeof cursor.result.value === 'string') { + items.set(cursor.result.key.toString(), cursor.result.value); + } + + // Advance cursor to next row + cursor.result.continue(); + } else { + resolve(items); // reached end of table } - }); + }; - this.cache.forEach((_, key) => { - if (!items.has(key)) { - deleted.add(key); - } - }); - } + const onError = (error: Error | null) => { + this.logService.error(`[IndexedDB Storage ${this.name}] getItems(): ${toErrorMessage(error, true)}`); - // no previous cache, consider all as changed - else { - changed = items; - } + resolve(items); + }; - // Update cache - this.cache = items; - - // Emit as event as needed - if (changed.size > 0 || deleted.size > 0) { - this._onDidChangeItemsExternal.fire({ changed, deleted }); - } - } - - async getItems(): Promise> { - if (!this.cache) { - try { - this.cache = await this.doGetItemsFromFile(); - } catch (error) { - this.cache = new Map(); - } - } - - return this.cache; - } - - private async doGetItemsFromFile(): Promise> { - await this.pendingUpdate; - - const itemsRaw = await this.fileService.readFile(this.file); - - this.ensureWatching(); // now that the file must exist, ensure we watch it for changes - - return new Map(JSON.parse(itemsRaw.value.toString())); + // Error handlers + cursor.onerror = () => onError(cursor.error); + transaction.onerror = () => onError(transaction.error); + }); } async updateItems(request: IUpdateRequest): Promise { - const items = await this.getItems(); + this.pendingUpdate = this.doUpdateItems(request); + try { + await this.pendingUpdate; + } finally { + this.pendingUpdate = undefined; + } + } - if (request.insert) { - request.insert.forEach((value, key) => items.set(key, value)); + private async doUpdateItems(request: IUpdateRequest): Promise { + + // Return early if the request is empty + const toInsert = request.insert; + const toDelete = request.delete; + if ((!toInsert && !toDelete) || (toInsert?.size === 0 && toDelete?.size === 0)) { + return; } - if (request.delete) { - request.delete.forEach(key => items.delete(key)); - } + // Update `ItemTable` with inserts and/or deletes + return new Promise(async (resolve, reject) => { + const db = await this.whenConnected; + const transaction = db.transaction(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE, 'readwrite'); + transaction.oncomplete = () => resolve(); + transaction.onerror = () => reject(transaction.error); + + const objectStore = transaction.objectStore(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE); + + // Inserts + if (toInsert) { + for (const [key, value] of toInsert) { + objectStore.put(value, key); + } + } + + // Deletes + if (toDelete) { + for (const key of toDelete) { + objectStore.delete(key); + } + } + }); + } + + async close(): Promise { + const db = await this.whenConnected; + + // Wait for pending updates to having finished await this.pendingUpdate; - this.pendingUpdate = (async () => { - try { - this._hasPendingUpdate = true; - - await this.fileService.writeFile(this.file, VSBuffer.fromString(JSON.stringify(Array.from(items.entries())))); - - this.ensureWatching(); // now that the file must exist, ensure we watch it for changes - } finally { - this._hasPendingUpdate = false; - } - })(); - - return this.pendingUpdate; + // Finally, close IndexedDB + return db.close(); } - close(): Promise { - return this.pendingUpdate; + clear(): Promise { + return new Promise(async (resolve, reject) => { + const db = await this.whenConnected; + + const transaction = db.transaction(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE, 'readwrite'); + transaction.oncomplete = () => resolve(); + transaction.onerror = () => reject(transaction.error); + + // Clear every row in the `ItemTable` + const objectStore = transaction.objectStore(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE); + objectStore.clear(); + }); } } diff --git a/src/vs/platform/storage/common/storageIpc.ts b/src/vs/platform/storage/common/storageIpc.ts index a95938a3ae..d633020112 100644 --- a/src/vs/platform/storage/common/storageIpc.ts +++ b/src/vs/platform/storage/common/storageIpc.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; diff --git a/src/vs/platform/storage/electron-main/storageIpc.ts b/src/vs/platform/storage/electron-main/storageIpc.ts index f8a6224c5c..f817d6d5b9 100644 --- a/src/vs/platform/storage/electron-main/storageIpc.ts +++ b/src/vs/platform/storage/electron-main/storageIpc.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; diff --git a/src/vs/platform/storage/electron-main/storageMain.ts b/src/vs/platform/storage/electron-main/storageMain.ts index 3cd5051587..a1f5cc008d 100644 --- a/src/vs/platform/storage/electron-main/storageMain.ts +++ b/src/vs/platform/storage/electron-main/storageMain.ts @@ -1,10 +1,9 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { promises } from 'fs'; -import { exists, writeFile } from 'vs/base/node/pfs'; +import { Promises } from 'vs/base/node/pfs'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; @@ -270,13 +269,13 @@ export class WorkspaceStorageMain extends BaseStorageMain implements IStorageMai const workspaceStorageFolderPath = join(this.environmentService.workspaceStorageHome.fsPath, this.workspace.id); const workspaceStorageDatabasePath = join(workspaceStorageFolderPath, WorkspaceStorageMain.WORKSPACE_STORAGE_NAME); - const storageExists = await exists(workspaceStorageFolderPath); + const storageExists = await Promises.exists(workspaceStorageFolderPath); if (storageExists) { return { storageFilePath: workspaceStorageDatabasePath, wasCreated: false }; } // Ensure storage folder exists - await promises.mkdir(workspaceStorageFolderPath, { recursive: true }); + await Promises.mkdir(workspaceStorageFolderPath, { recursive: true }); // Write metadata into folder (but do not await) this.ensureWorkspaceStorageFolderMeta(workspaceStorageFolderPath); @@ -295,9 +294,9 @@ export class WorkspaceStorageMain extends BaseStorageMain implements IStorageMai if (meta) { try { const workspaceStorageMetaPath = join(workspaceStorageFolderPath, WorkspaceStorageMain.WORKSPACE_META_NAME); - const storageExists = await exists(workspaceStorageMetaPath); + const storageExists = await Promises.exists(workspaceStorageMetaPath); if (!storageExists) { - await writeFile(workspaceStorageMetaPath, JSON.stringify(meta, undefined, 2)); + await Promises.writeFile(workspaceStorageMetaPath, JSON.stringify(meta, undefined, 2)); } } catch (error) { this.logService.error(`StorageMain#ensureWorkspaceStorageFolderMeta(): Unable to create workspace storage metadata due to ${error}`); diff --git a/src/vs/platform/storage/electron-main/storageMainService.ts b/src/vs/platform/storage/electron-main/storageMainService.ts index 35ede6a0ea..1a27b9c169 100644 --- a/src/vs/platform/storage/electron-main/storageMainService.ts +++ b/src/vs/platform/storage/electron-main/storageMainService.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { once } from 'vs/base/common/functional'; diff --git a/src/vs/platform/storage/electron-sandbox/storageService.ts b/src/vs/platform/storage/electron-sandbox/storageService.ts index 10520a8715..089624996a 100644 --- a/src/vs/platform/storage/electron-sandbox/storageService.ts +++ b/src/vs/platform/storage/electron-sandbox/storageService.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { MutableDisposable } from 'vs/base/common/lifecycle'; diff --git a/src/vs/platform/storage/test/browser/storageService.test.ts b/src/vs/platform/storage/test/browser/storageService.test.ts index d95911311c..e0d64c9ce7 100644 --- a/src/vs/platform/storage/test/browser/storageService.test.ts +++ b/src/vs/platform/storage/test/browser/storageService.test.ts @@ -4,88 +4,158 @@ *--------------------------------------------------------------------------------------------*/ import { strictEqual } from 'assert'; -import { BrowserStorageService, FileStorageDatabase } from 'vs/platform/storage/browser/storageService'; +import { BrowserStorageService, IndexedDBStorageDatabase } from 'vs/platform/storage/browser/storageService'; import { NullLogService } from 'vs/platform/log/common/log'; import { Storage } from 'vs/base/parts/storage/common/storage'; -import { URI } from 'vs/base/common/uri'; -import { FileService } from 'vs/platform/files/common/fileService'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { Schemas } from 'vs/base/common/network'; -import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { createSuite } from 'vs/platform/storage/test/common/storageService.test'; +import { flakySuite } from 'vs/base/test/common/testUtils'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { Schemas } from 'vs/base/common/network'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { URI } from 'vs/base/common/uri'; +import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -suite('StorageService (browser)', function () { +async function createStorageService(): Promise<[DisposableStore, BrowserStorageService]> { + const disposables = new DisposableStore(); + const logService = new NullLogService(); + const fileService = disposables.add(new FileService(logService)); + + const userDataProvider = disposables.add(new InMemoryFileSystemProvider()); + disposables.add(fileService.registerProvider(Schemas.userData, userDataProvider)); + + const storageService = disposables.add(new BrowserStorageService({ id: 'workspace-storage-test' }, logService, { userRoamingDataHome: URI.file('/User').with({ scheme: Schemas.userData }) } as unknown as IEnvironmentService, fileService)); + + await storageService.initialize(); + + return [disposables, storageService]; +} + +flakySuite('StorageService (browser)', function () { const disposables = new DisposableStore(); let storageService: BrowserStorageService; createSuite({ setup: async () => { - const logService = new NullLogService(); - - const fileService = disposables.add(new FileService(logService)); - - const userDataProvider = disposables.add(new InMemoryFileSystemProvider()); - disposables.add(fileService.registerProvider(Schemas.userData, userDataProvider)); - - storageService = disposables.add(new BrowserStorageService({ id: String(Date.now()) }, { userRoamingDataHome: URI.file('/User').with({ scheme: Schemas.userData }) } as unknown as IEnvironmentService, fileService)); - - await storageService.initialize(); + const res = await createStorageService(); + disposables.add(res[0]); + storageService = res[1]; return storageService; }, - teardown: async storage => { - await storageService.flush(); + teardown: async () => { + await storageService.clear(); disposables.clear(); } }); }); -suite('FileStorageDatabase (browser)', () => { - - let fileService: FileService; - +flakySuite('StorageService (browser specific)', () => { const disposables = new DisposableStore(); + let storageService: BrowserStorageService; setup(async () => { - const logService = new NullLogService(); + const res = await createStorageService(); + disposables.add(res[0]); - fileService = disposables.add(new FileService(logService)); - - const userDataProvider = disposables.add(new InMemoryFileSystemProvider()); - disposables.add(fileService.registerProvider(Schemas.userData, userDataProvider)); + storageService = res[1]; }); - teardown(() => { + teardown(async () => { + await storageService.clear(); disposables.clear(); }); - test('Basics', async () => { - const testDir = URI.file('/User/storage.json').with({ scheme: Schemas.userData }); + test('clear', async () => { + storageService.store('bar', 'foo', StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store('bar', 3, StorageScope.GLOBAL, StorageTarget.USER); + storageService.store('bar', 'foo', StorageScope.WORKSPACE, StorageTarget.MACHINE); + storageService.store('bar', 3, StorageScope.WORKSPACE, StorageTarget.USER); - let storage = new Storage(new FileStorageDatabase(testDir, false, fileService)); + await storageService.clear(); + + for (const scope of [StorageScope.GLOBAL, StorageScope.WORKSPACE]) { + for (const target of [StorageTarget.USER, StorageTarget.MACHINE]) { + strictEqual(storageService.get('bar', scope), undefined); + strictEqual(storageService.keys(scope, target).length, 0); + } + } + }); +}); + +flakySuite('IndexDBStorageDatabase (browser)', () => { + + const id = 'workspace-storage-db-test'; + const logService = new NullLogService(); + + teardown(async () => { + const storage = await IndexedDBStorageDatabase.create(id, logService); + await storage.clear(); + }); + + test('Basics', async () => { + let storage = new Storage(await IndexedDBStorageDatabase.create(id, logService)); await storage.init(); + // Insert initial data storage.set('bar', 'foo'); storage.set('barNumber', 55); storage.set('barBoolean', true); + storage.set('barUndefined', undefined); + storage.set('barNull', null); strictEqual(storage.get('bar'), 'foo'); strictEqual(storage.get('barNumber'), '55'); strictEqual(storage.get('barBoolean'), 'true'); + strictEqual(storage.get('barUndefined'), undefined); + strictEqual(storage.get('barNull'), undefined); + + strictEqual(storage.size, 3); + strictEqual(storage.items.size, 3); await storage.close(); - storage = new Storage(new FileStorageDatabase(testDir, false, fileService)); + storage = new Storage(await IndexedDBStorageDatabase.create(id, logService)); await storage.init(); + // Check initial data still there strictEqual(storage.get('bar'), 'foo'); strictEqual(storage.get('barNumber'), '55'); strictEqual(storage.get('barBoolean'), 'true'); + strictEqual(storage.get('barUndefined'), undefined); + strictEqual(storage.get('barNull'), undefined); + strictEqual(storage.size, 3); + strictEqual(storage.items.size, 3); + + // Update data + storage.set('bar', 'foo2'); + storage.set('barNumber', 552); + + strictEqual(storage.get('bar'), 'foo2'); + strictEqual(storage.get('barNumber'), '552'); + + await storage.close(); + + storage = new Storage(await IndexedDBStorageDatabase.create(id, logService)); + + await storage.init(); + + // Check initial data still there + strictEqual(storage.get('bar'), 'foo2'); + strictEqual(storage.get('barNumber'), '552'); + strictEqual(storage.get('barBoolean'), 'true'); + strictEqual(storage.get('barUndefined'), undefined); + strictEqual(storage.get('barNull'), undefined); + + strictEqual(storage.size, 3); + strictEqual(storage.items.size, 3); + + // Delete data storage.delete('bar'); storage.delete('barNumber'); storage.delete('barBoolean'); @@ -94,14 +164,82 @@ suite('FileStorageDatabase (browser)', () => { strictEqual(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber'); strictEqual(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean'); + strictEqual(storage.size, 0); + strictEqual(storage.items.size, 0); + await storage.close(); - storage = new Storage(new FileStorageDatabase(testDir, false, fileService)); + storage = new Storage(await IndexedDBStorageDatabase.create(id, logService)); await storage.init(); strictEqual(storage.get('bar', 'undefined'), 'undefined'); strictEqual(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber'); strictEqual(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean'); + + strictEqual(storage.size, 0); + strictEqual(storage.items.size, 0); + }); + + test('Clear', async () => { + let storage = new Storage(await IndexedDBStorageDatabase.create(id, logService)); + + await storage.init(); + + storage.set('bar', 'foo'); + storage.set('barNumber', 55); + storage.set('barBoolean', true); + + await storage.close(); + + const db = await IndexedDBStorageDatabase.create(id, logService); + storage = new Storage(db); + + await storage.init(); + await db.clear(); + + storage = new Storage(await IndexedDBStorageDatabase.create(id, logService)); + + await storage.init(); + + strictEqual(storage.get('bar'), undefined); + strictEqual(storage.get('barNumber'), undefined); + strictEqual(storage.get('barBoolean'), undefined); + + strictEqual(storage.size, 0); + strictEqual(storage.items.size, 0); + }); + + test('Inserts and Deletes at the same time', async () => { + let storage = new Storage(await IndexedDBStorageDatabase.create(id, logService)); + + await storage.init(); + + storage.set('bar', 'foo'); + storage.set('barNumber', 55); + storage.set('barBoolean', true); + + await storage.close(); + + storage = new Storage(await IndexedDBStorageDatabase.create(id, logService)); + + await storage.init(); + + storage.set('bar', 'foobar'); + const largeItem = JSON.stringify({ largeItem: 'Hello World'.repeat(1000) }); + storage.set('largeItem', largeItem); + storage.delete('barNumber'); + storage.delete('barBoolean'); + + await storage.close(); + + storage = new Storage(await IndexedDBStorageDatabase.create(id, logService)); + + await storage.init(); + + strictEqual(storage.get('bar'), 'foobar'); + strictEqual(storage.get('largeItem'), largeItem); + strictEqual(storage.get('barNumber'), undefined); + strictEqual(storage.get('barBoolean'), undefined); }); }); diff --git a/src/vs/platform/storage/test/electron-main/storageMainService.test.ts b/src/vs/platform/storage/test/electron-main/storageMainService.test.ts index 0f55ca5c3b..2b8ef013e3 100644 --- a/src/vs/platform/storage/test/electron-main/storageMainService.test.ts +++ b/src/vs/platform/storage/test/electron-main/storageMainService.test.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { notStrictEqual, strictEqual } from 'assert'; @@ -66,8 +66,8 @@ suite('StorageMainService', function () { registerWindow(window: ICodeWindow): void { } async reload(window: ICodeWindow, cli?: NativeParsedArgs): Promise { } async unload(window: ICodeWindow, reason: UnloadReason): Promise { return true; } - relaunch(options?: { addArgs?: string[] | undefined; removeArgs?: string[] | undefined; }): void { } - async quit(fromUpdate?: boolean): Promise { return true; } + async relaunch(options?: { addArgs?: string[] | undefined; removeArgs?: string[] | undefined; }): Promise { } + async quit(willRestart?: boolean): Promise { return true; } async kill(code?: number): Promise { } async when(phase: LifecycleMainPhase): Promise { } } diff --git a/src/vs/platform/telemetry/common/commonProperties.ts b/src/vs/platform/telemetry/common/commonProperties.ts index d38f1b24c6..93a3e1dd06 100644 --- a/src/vs/platform/telemetry/common/commonProperties.ts +++ b/src/vs/platform/telemetry/common/commonProperties.ts @@ -105,7 +105,7 @@ export async function resolveCommonProperties( return result; } -function verifyMicrosoftInternalDomain(domainList: readonly string[]): boolean { +export function verifyMicrosoftInternalDomain(domainList: readonly string[]): boolean { const userDnsDomain = env['USERDNSDOMAIN']; if (!userDnsDomain) { return false; diff --git a/src/vs/platform/telemetry/electron-sandbox/customEndpointTelemetryService.ts b/src/vs/platform/telemetry/electron-sandbox/customEndpointTelemetryService.ts index 210cdb4045..20c20d07d2 100644 --- a/src/vs/platform/telemetry/electron-sandbox/customEndpointTelemetryService.ts +++ b/src/vs/platform/telemetry/electron-sandbox/customEndpointTelemetryService.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { registerSharedProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services'; diff --git a/src/vs/platform/telemetry/node/customEndpointTelemetryService.ts b/src/vs/platform/telemetry/node/customEndpointTelemetryService.ts index d64fb6b223..0724353876 100644 --- a/src/vs/platform/telemetry/node/customEndpointTelemetryService.ts +++ b/src/vs/platform/telemetry/node/customEndpointTelemetryService.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Client as TelemetryClient } from 'vs/base/parts/ipc/node/ipc.cp'; diff --git a/src/vs/platform/telemetry/node/telemetry.ts b/src/vs/platform/telemetry/node/telemetry.ts index 9e0504d556..72fedf07d0 100644 --- a/src/vs/platform/telemetry/node/telemetry.ts +++ b/src/vs/platform/telemetry/node/telemetry.ts @@ -3,43 +3,52 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { readdirSync } from 'vs/base/node/pfs'; -import { statSync, readFileSync } from 'fs'; +import { Promises } from 'vs/base/node/pfs'; import { join } from 'vs/base/common/path'; -export function buildTelemetryMessage(appRoot: string, extensionsPath?: string): string { +export async function buildTelemetryMessage(appRoot: string, extensionsPath?: string): Promise { const mergedTelemetry = Object.create(null); + // Simple function to merge the telemetry into one json object const mergeTelemetry = (contents: string, dirName: string) => { const telemetryData = JSON.parse(contents); mergedTelemetry[dirName] = telemetryData; }; + if (extensionsPath) { - // Gets all the directories inside the extension directory - const dirs = readdirSync(extensionsPath).filter(files => { - // This handles case where broken symbolic links can cause statSync to throw and error + const dirs: string[] = []; + + const files = await Promises.readdir(extensionsPath); + for (const file of files) { try { - return statSync(join(extensionsPath, files)).isDirectory(); + const fileStat = await Promises.stat(join(extensionsPath, file)); + if (fileStat.isDirectory()) { + dirs.push(file); + } } catch { - return false; + // This handles case where broken symbolic links can cause statSync to throw and error } - }); + } + const telemetryJsonFolders: string[] = []; - dirs.forEach((dir) => { - const files = readdirSync(join(extensionsPath, dir)).filter(file => file === 'telemetry.json'); - // We know it contains a telemetry.json file so we add it to the list of folders which have one + for (const dir of dirs) { + const files = (await Promises.readdir(join(extensionsPath, dir))).filter(file => file === 'telemetry.json'); if (files.length === 1) { - telemetryJsonFolders.push(dir); + telemetryJsonFolders.push(dir); // // We know it contains a telemetry.json file so we add it to the list of folders which have one } - }); - telemetryJsonFolders.forEach((folder) => { - const contents = readFileSync(join(extensionsPath, folder, 'telemetry.json')).toString(); + } + + for (const folder of telemetryJsonFolders) { + const contents = (await Promises.readFile(join(extensionsPath, folder, 'telemetry.json'))).toString(); mergeTelemetry(contents, folder); - }); + } } - let contents = readFileSync(join(appRoot, 'telemetry-core.json')).toString(); + + let contents = (await Promises.readFile(join(appRoot, 'telemetry-core.json'))).toString(); mergeTelemetry(contents, 'vscode-core'); - contents = readFileSync(join(appRoot, 'telemetry-extensions.json')).toString(); + + contents = (await Promises.readFile(join(appRoot, 'telemetry-extensions.json'))).toString(); mergeTelemetry(contents, 'vscode-extensions'); + return JSON.stringify(mergedTelemetry, null, 4); } diff --git a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts index cea696c2f7..737ee75af2 100644 --- a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts +++ b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts @@ -9,10 +9,13 @@ import ErrorTelemetry from 'vs/platform/telemetry/browser/errorTelemetry'; import { NullAppender, ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils'; import * as Errors from 'vs/base/common/errors'; import * as sinon from 'sinon'; +import * as sinonTest from 'sinon-test'; import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +const sinonTestFn = sinonTest(sinon); + class TestTelemetryAppender implements ITelemetryAppender { public events: any[]; @@ -85,7 +88,7 @@ class ErrorTestingSettings { suite('TelemetryService', () => { - test('Disposing', sinon.test(function () { + test('Disposing', sinonTestFn(function () { let testAppender = new TestTelemetryAppender(); let service = new TelemetryService({ appender: testAppender }, undefined!); @@ -98,7 +101,7 @@ suite('TelemetryService', () => { })); // event reporting - test('Simple event', sinon.test(function () { + test('Simple event', sinonTestFn(function () { let testAppender = new TestTelemetryAppender(); let service = new TelemetryService({ appender: testAppender }, undefined!); @@ -111,7 +114,7 @@ suite('TelemetryService', () => { }); })); - test('Event with data', sinon.test(function () { + test('Event with data', sinonTestFn(function () { let testAppender = new TestTelemetryAppender(); let service = new TelemetryService({ appender: testAppender }, undefined!); @@ -193,7 +196,7 @@ suite('TelemetryService', () => { }); }); - test('enableTelemetry on by default', sinon.test(function () { + test('enableTelemetry on by default', sinonTestFn(function () { let testAppender = new TestTelemetryAppender(); let service = new TelemetryService({ appender: testAppender }, undefined!); @@ -224,7 +227,7 @@ suite('TelemetryService', () => { } } - test.skip('Error events', sinon.test(async function (this: any) { // {{SQL CARBON EIDT}} skip test + test.skip('Error events', sinonTestFn(async function (this: any) { // {{SQL CARBON EDIT}} skip test let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); Errors.setUnexpectedErrorHandler(() => { }); @@ -256,7 +259,7 @@ suite('TelemetryService', () => { } })); - // test('Unhandled Promise Error events', sinon.test(function() { + // test('Unhandled Promise Error events', sinonTestFn(function() { // // let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); // Errors.setUnexpectedErrorHandler(() => {}); @@ -285,7 +288,7 @@ suite('TelemetryService', () => { // } // })); - test.skip('Handle global errors', sinon.test(async function (this: any) { // {{SQL CARBON EIDT}} skip test + test.skip('Handle global errors', sinonTestFn(async function (this: any) { // {{SQL CARBON EDIT}} skip test let errorStub = sinon.stub(); window.onerror = errorStub; @@ -313,7 +316,7 @@ suite('TelemetryService', () => { service.dispose(); })); - test.skip('Error Telemetry removes PII from filename with spaces', sinon.test(async function (this: any) { // {{SQL CARBON EIDT}} skip test + test.skip('Error Telemetry removes PII from filename with spaces', sinonTestFn(async function (this: any) { // {{SQL CARBON EDIT}} skip test let errorStub = sinon.stub(); window.onerror = errorStub; let settings = new ErrorTestingSettings(); @@ -336,7 +339,7 @@ suite('TelemetryService', () => { service.dispose(); })); - test.skip('Uncaught Error Telemetry removes PII from filename', sinon.test(function (this: any) { // {{SQL CARBON EIDT}} skip test + test.skip('Uncaught Error Telemetry removes PII from filename', sinonTestFn(function (this: any) { // {{SQL CARBON EDIT}} skip test let clock = this.clock; let errorStub = sinon.stub(); window.onerror = errorStub; @@ -368,7 +371,7 @@ suite('TelemetryService', () => { }); })); - test.skip('Unexpected Error Telemetry removes PII', sinon.test(async function (this: any) { // {{SQL CARBON EIDT}} skip test + test.skip('Unexpected Error Telemetry removes PII', sinonTestFn(async function (this: any) { // {{SQL CARBON EDIT}} skip test let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); Errors.setUnexpectedErrorHandler(() => { }); try { @@ -399,7 +402,7 @@ suite('TelemetryService', () => { } })); - test.skip('Uncaught Error Telemetry removes PII', sinon.test(async function (this: any) { // {{SQL CARBON EIDT}} skip test + test.skip('Uncaught Error Telemetry removes PII', sinonTestFn(async function (this: any) { // {{SQL CARBON EDIT}} skip test let errorStub = sinon.stub(); window.onerror = errorStub; let settings = new ErrorTestingSettings(); @@ -426,7 +429,7 @@ suite('TelemetryService', () => { service.dispose(); })); - test.skip('Unexpected Error Telemetry removes PII but preserves Code file path', sinon.test(async function (this: any) { // {{SQL CARBON EIDT}} skip test + test.skip('Unexpected Error Telemetry removes PII but preserves Code file path', sinonTestFn(async function (this: any) { // {{SQL CARBON EDIT}} skip test let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); Errors.setUnexpectedErrorHandler(() => { }); @@ -462,7 +465,7 @@ suite('TelemetryService', () => { } })); - test.skip('Uncaught Error Telemetry removes PII but preserves Code file path', sinon.test(async function (this: any) { // {{SQL CARBON EIDT}} skip test + test.skip('Uncaught Error Telemetry removes PII but preserves Code file path', sinonTestFn(async function (this: any) { // {{SQL CARBON EDIT}} skip test let errorStub = sinon.stub(); window.onerror = errorStub; let settings = new ErrorTestingSettings(); @@ -491,7 +494,7 @@ suite('TelemetryService', () => { service.dispose(); })); - test.skip('Unexpected Error Telemetry removes PII but preserves Code file path with node modules', sinon.test(async function (this: any) { // {{SQL CARBON EIDT}} skip test + test.skip('Unexpected Error Telemetry removes PII but preserves Code file path with node modules', sinonTestFn(async function (this: any) { // {{SQL CARBON EDIT}} skip test let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); Errors.setUnexpectedErrorHandler(() => { }); @@ -523,7 +526,7 @@ suite('TelemetryService', () => { } })); - test.skip('Uncaught Error Telemetry removes PII but preserves Code file path', sinon.test(async function (this: any) { // {{SQL CARBON EIDT}} skip test + test.skip('Uncaught Error Telemetry removes PII but preserves Code file path', sinonTestFn(async function (this: any) { // {{SQL CARBON EDIT}} skip test let errorStub = sinon.stub(); window.onerror = errorStub; let settings = new ErrorTestingSettings(); @@ -549,7 +552,7 @@ suite('TelemetryService', () => { })); - test.skip('Unexpected Error Telemetry removes PII but preserves Code file path when PIIPath is configured', sinon.test(async function (this: any) { // {{SQL CARBON EIDT}} skip test + test.skip('Unexpected Error Telemetry removes PII but preserves Code file path when PIIPath is configured', sinonTestFn(async function (this: any) { // {{SQL CARBON EDIT}} skip test let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); Errors.setUnexpectedErrorHandler(() => { }); @@ -585,7 +588,7 @@ suite('TelemetryService', () => { } })); - test.skip('Uncaught Error Telemetry removes PII but preserves Code file path when PIIPath is configured', sinon.test(async function (this: any) { // {{SQL CARBON EIDT}} skip test + test.skip('Uncaught Error Telemetry removes PII but preserves Code file path when PIIPath is configured', sinonTestFn(async function (this: any) { // {{SQL CARBON EDIT}} skip test let errorStub = sinon.stub(); window.onerror = errorStub; let settings = new ErrorTestingSettings(); @@ -614,7 +617,7 @@ suite('TelemetryService', () => { service.dispose(); })); - test.skip('Unexpected Error Telemetry removes PII but preserves Missing Model error message', sinon.test(async function (this: any) { // {{SQL CARBON EIDT}} skip test + test.skip('Unexpected Error Telemetry removes PII but preserves Missing Model error message', sinonTestFn(async function (this: any) { // {{SQL CARBON EDIT}} skip test let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); Errors.setUnexpectedErrorHandler(() => { }); @@ -650,7 +653,7 @@ suite('TelemetryService', () => { } })); - test.skip('Uncaught Error Telemetry removes PII but preserves Missing Model error message', sinon.test(async function (this: any) { // {{SQL CARBON EIDT}} skip test + test.skip('Uncaught Error Telemetry removes PII but preserves Missing Model error message', sinonTestFn(async function (this: any) { // {{SQL CARBON EDIT}} skip test let errorStub = sinon.stub(); window.onerror = errorStub; let settings = new ErrorTestingSettings(); @@ -680,7 +683,7 @@ suite('TelemetryService', () => { service.dispose(); })); - test.skip('Unexpected Error Telemetry removes PII but preserves No Such File error message', sinon.test(async function (this: any) { // {{SQL CARBON EIDT}} skip test + test.skip('Unexpected Error Telemetry removes PII but preserves No Such File error message', sinonTestFn(async function (this: any) { // {{SQL CARBON EDIT}} skip test let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); Errors.setUnexpectedErrorHandler(() => { }); @@ -715,7 +718,8 @@ suite('TelemetryService', () => { Errors.setUnexpectedErrorHandler(origErrorHandler); } })); - test.skip('Uncaught Error Telemetry removes PII but preserves No Such File error message', sinon.test(async function (this: any) { // {{SQL CARBON EDIT}} skip tests + + test.skip('Uncaught Error Telemetry removes PII but preserves No Such File error message', sinonTestFn(async function (this: any) { // {{SQL CARBON EDIT}} skip test let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); Errors.setUnexpectedErrorHandler(() => { }); try { @@ -751,7 +755,7 @@ suite('TelemetryService', () => { } })); - test('Telemetry Service sends events when enableTelemetry is on', sinon.test(function () { + test('Telemetry Service sends events when enableTelemetry is on', sinonTestFn(function () { let testAppender = new TestTelemetryAppender(); let service = new TelemetryService({ appender: testAppender }, undefined!); diff --git a/src/vs/platform/terminal/common/environmentVariable.ts b/src/vs/platform/terminal/common/environmentVariable.ts index 455925cfb4..1421b9f17b 100644 --- a/src/vs/platform/terminal/common/environmentVariable.ts +++ b/src/vs/platform/terminal/common/environmentVariable.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ export enum EnvironmentVariableMutatorType { diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 28a4c9b649..9aae05bf5e 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -8,6 +8,86 @@ import { Event } from 'vs/base/common/event'; import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; + +export const enum TerminalSettingPrefix { + Shell = 'terminal.integrated.shell.', + ShellArgs = 'terminal.integrated.shellArgs.', + DefaultProfile = 'terminal.integrated.defaultProfile.', + Profiles = 'terminal.integrated.profiles.' +} + +export const enum TerminalSettingId { + ShellLinux = 'terminal.integrated.shell.linux', + ShellMacOs = 'terminal.integrated.shell.osx', + ShellWindows = 'terminal.integrated.shell.windows', + SendKeybindingsToShell = 'terminal.integrated.sendKeybindingsToShell', + AutomationShellLinux = 'terminal.integrated.automationShell.linux', + AutomationShellMacOs = 'terminal.integrated.automationShell.osx', + AutomationShellWindows = 'terminal.integrated.automationShell.windows', + ShellArgsLinux = 'terminal.integrated.shellArgs.linux', + ShellArgsMacOs = 'terminal.integrated.shellArgs.osx', + ShellArgsWindows = 'terminal.integrated.shellArgs.windows', + ProfilesWindows = 'terminal.integrated.profiles.windows', + ProfilesMacOs = 'terminal.integrated.profiles.osx', + ProfilesLinux = 'terminal.integrated.profiles.linux', + DefaultProfileLinux = 'terminal.integrated.defaultProfile.linux', + DefaultProfileMacOs = 'terminal.integrated.defaultProfile.osx', + DefaultProfileWindows = 'terminal.integrated.defaultProfile.windows', + UseWslProfiles = 'terminal.integrated.useWslProfiles', + TabsEnabled = 'terminal.integrated.tabs.enabled', + TabsHideCondition = 'terminal.integrated.tabs.hideCondition', + TabsShowActiveTerminal = 'terminal.integrated.tabs.showActiveTerminal', + TabsLocation = 'terminal.integrated.tabs.location', + TabsFocusMode = 'terminal.integrated.tabs.focusMode', + MacOptionIsMeta = 'terminal.integrated.macOptionIsMeta', + MacOptionClickForcesSelection = 'terminal.integrated.macOptionClickForcesSelection', + AltClickMovesCursor = 'terminal.integrated.altClickMovesCursor', + CopyOnSelection = 'terminal.integrated.copyOnSelection', + DrawBoldTextInBrightColors = 'terminal.integrated.drawBoldTextInBrightColors', + FontFamily = 'terminal.integrated.fontFamily', + FontSize = 'terminal.integrated.fontSize', + LetterSpacing = 'terminal.integrated.letterSpacing', + LineHeight = 'terminal.integrated.lineHeight', + MinimumContrastRatio = 'terminal.integrated.minimumContrastRatio', + FastScrollSensitivity = 'terminal.integrated.fastScrollSensitivity', + MouseWheelScrollSensitivity = 'terminal.integrated.mouseWheelScrollSensitivity', + BellDuration = 'terminal.integrated.bellDuration', + FontWeight = 'terminal.integrated.fontWeight', + FontWeightBold = 'terminal.integrated.fontWeightBold', + CursorBlinking = 'terminal.integrated.cursorBlinking', + CursorStyle = 'terminal.integrated.cursorStyle', + CursorWidth = 'terminal.integrated.cursorWidth', + Scrollback = 'terminal.integrated.scrollback', + DetectLocale = 'terminal.integrated.detectLocale', + GpuAcceleration = 'terminal.integrated.gpuAcceleration', + RightClickBehavior = 'terminal.integrated.rightClickBehavior', + Cwd = 'terminal.integrated.cwd', + ConfirmOnExit = 'terminal.integrated.confirmOnExit', + EnableBell = 'terminal.integrated.enableBell', + CommandsToSkipShell = 'terminal.integrated.commandsToSkipShell', + AllowChords = 'terminal.integrated.allowChords', + AllowMnemonics = 'terminal.integrated.allowMnemonics', + EnvMacOs = 'terminal.integrated.env.osx', + EnvLinux = 'terminal.integrated.env.linux', + EnvWindows = 'terminal.integrated.env.windows', + EnvironmentChangesIndicator = 'terminal.integrated.environmentChangesIndicator', + EnvironmentChangesRelaunch = 'terminal.integrated.environmentChangesRelaunch', + ShowExitAlert = 'terminal.integrated.showExitAlert', + SplitCwd = 'terminal.integrated.splitCwd', + WindowsEnableConpty = 'terminal.integrated.windowsEnableConpty', + WordSeparators = 'terminal.integrated.wordSeparators', + TitleMode = 'terminal.integrated.titleMode', + EnableFileLinks = 'terminal.integrated.enableFileLinks', + UnicodeVersion = 'terminal.integrated.unicodeVersion', + ExperimentalLinkProvider = 'terminal.integrated.experimentalLinkProvider', + LocalEchoLatencyThreshold = 'terminal.integrated.localEchoLatencyThreshold', + LocalEchoExcludePrograms = 'terminal.integrated.localEchoExcludePrograms', + LocalEchoStyle = 'terminal.integrated.localEchoStyle', + EnablePersistentSessions = 'terminal.integrated.enablePersistentSessions', + InheritEnv = 'terminal.integrated.inheritEnv', + ShowLinkHover = 'terminal.integrated.showLinkHover', +} export enum WindowsShellType { CommandPrompt = 'cmd', @@ -30,7 +110,6 @@ export interface IRawTerminalTabLayoutInfo { } export type ITerminalTabLayoutInfoById = IRawTerminalTabLayoutInfo; -export type ITerminalTabLayoutInfo = IRawTerminalTabLayoutInfo; export interface IRawTerminalsLayoutInfo { tabs: IRawTerminalTabLayoutInfo[]; @@ -40,11 +119,21 @@ export interface IPtyHostAttachTarget { id: number; pid: number; title: string; + titleSource: TitleEventSource; cwd: string; workspaceId: string; workspaceName: string; isOrphan: boolean; - icon: string | undefined; + icon: TerminalIcon | undefined; +} + +export enum TitleEventSource { + /** From the API or the rename command that overrides any other type */ + Api, + /** From the process name property*/ + Process, + /** From the VT sequence */ + Sequence } export type ITerminalsLayoutInfo = IRawTerminalsLayoutInfo; @@ -96,8 +185,13 @@ export interface IOffProcessTerminalService { attachToProcess(id: number): Promise; listProcesses(): Promise; getDefaultSystemShell(osOverride?: OperatingSystem): Promise; - getShellEnvironment(): Promise; + getProfiles(profiles: unknown, defaultProfile: unknown, includeDetectedProfiles?: boolean): Promise; + getWslPath(original: string): Promise; + getEnvironment(): Promise; + getShellEnvironment(): Promise; setTerminalLayoutInfo(layoutInfo?: ITerminalsLayoutInfoById): Promise; + updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise; + updateIcon(id: number, icon: TerminalIcon, color?: string): Promise; getTerminalLayoutInfo(): Promise; reduceConnectionGraceTime(): Promise; } @@ -115,6 +209,8 @@ export interface IPtyService { readonly onPtyHostStart?: Event; readonly onPtyHostUnresponsive?: Event; readonly onPtyHostResponsive?: Event; + readonly onPtyHostRequestResolveVariables?: Event; + readonly onProcessData: Event<{ id: number, event: IProcessDataEvent | string }>; readonly onProcessExit: Event<{ id: number, event: number | undefined }>; readonly onProcessReady: Event<{ id: number, event: { pid: number, cwd: string } }>; @@ -127,6 +223,7 @@ export interface IPtyService { restartPtyHost?(): Promise; shutdownAll?(): Promise; + acceptPtyHostResolvedVariables?(id: number, resolved: string[]): Promise; createProcess( shellLaunchConfig: IShellLaunchConfig, @@ -159,14 +256,22 @@ export interface IPtyService { processBinary(id: number, data: string): Promise; /** Confirm the process is _not_ an orphan. */ orphanQuestionReply(id: number): Promise; - + updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise; + updateIcon(id: number, icon: TerminalIcon, color?: string): Promise; getDefaultSystemShell(osOverride?: OperatingSystem): Promise; - getShellEnvironment(): Promise; + getProfiles?(profiles: unknown, defaultProfile: unknown, includeDetectedProfiles?: boolean): Promise; + getEnvironment(): Promise; + getWslPath(original: string): Promise; setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise; getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise; reduceConnectionGraceTime(): Promise; } +export interface IRequestResolveVariablesEvent { + id: number; + originalText: string[]; +} + export enum HeartbeatConstants { /** * The duration between heartbeats @@ -260,7 +365,7 @@ export interface IShellLaunchConfig { /** * This is a terminal that attaches to an already running terminal. */ - attachPersistentProcess?: { id: number; pid: number; title: string; cwd: string; icon?: string; }; + attachPersistentProcess?: { id: number; pid: number; title: string; titleSource: TitleEventSource; cwd: string; icon?: TerminalIcon; color?: string }; /** * Whether the terminal process environment should be exactly as provided in @@ -271,6 +376,14 @@ export interface IShellLaunchConfig { */ strictEnv?: boolean; + /** + * Whether the terminal process environment will inherit VS Code's "shell environment" that may + * get sourced from running a login shell depnding on how the application was launched. + * Consumers that rely on development tools being present in the $PATH should set this to true. + * This will overwrite the value of the inheritEnv setting. + */ + useShellEnvironment?: boolean; + /** * When enabled the terminal will run the process as normal but not be surfaced to the user * until `Terminal.show` is called. The typical usage for this is when you need to run @@ -292,18 +405,25 @@ export interface IShellLaunchConfig { isExtensionOwnedTerminal?: boolean; /** - * The codicon ID to use for this terminal. If not specified it will use the default fallback - * icon. + * The icon for the terminal, used primarily in the terminal tab. */ - icon?: string; + icon?: TerminalIcon; + + /** + * The color ID to use for this terminal. If not specified it will use the default fallback + */ + color?: string; } +export type TerminalIcon = ThemeIcon | URI | { light: URI; dark: URI }; + export interface IShellLaunchConfigDto { name?: string; executable?: string; args?: string[] | string; cwd?: string | UriComponents; env?: ITerminalEnvironment; + useShellEnvironment?: boolean; hideFromUser?: boolean; } @@ -316,6 +436,12 @@ export interface ITerminalLaunchError { code?: number; } +export interface IProcessReadyEvent { + pid: number, + cwd: string, + requiresWindowsMode?: boolean +} + /** * An interface representing a raw terminal child process, this contains a subset of the * child_process.ChildProcess node.js interface. @@ -335,7 +461,7 @@ export interface ITerminalChildProcess { onProcessData: Event; onProcessExit: Event; - onProcessReady: Event<{ pid: number, cwd: string }>; + onProcessReady: Event; onProcessTitleChanged: Event; onProcessOverrideDimensions?: Event; onProcessResolvedShellLaunchConfig?: Event; @@ -378,15 +504,20 @@ export interface ITerminalChildProcess { getLatency(): Promise; } +export interface IReconnectConstants { + GraceTime: number, + ShortGraceTime: number +} + export const enum LocalReconnectConstants { /** * If there is no reconnection within this time-frame, consider the connection permanently closed... */ - ReconnectionGraceTime = 60000, // 60 seconds + GraceTime = 60000, // 60 seconds /** * Maximal grace time between the first and the last reconnection... */ - ReconnectionShortGraceTime = 6000, // 6 seconds + ShortGraceTime = 6000, // 6 seconds } export const enum FlowControlConstants { @@ -433,6 +564,18 @@ export interface ITerminalDimensions { rows: number; } +export interface ITerminalProfile { + profileName: string; + path: string; + isDefault: boolean; + isAutoDetected?: boolean; + args?: string | string[] | undefined; + env?: ITerminalEnvironment; + overrideName?: boolean; + color?: string; + icon?: ThemeIcon | URI | { light: URI, dark: URI }; +} + export interface ITerminalDimensionsOverride extends Readonly { /** * indicate that xterm must receive these exact dimensions, even if they overflow the ui! @@ -440,4 +583,26 @@ export interface ITerminalDimensionsOverride extends Readonly(key: string) => T | undefined; +export const enum ProfileSource { + GitBash = 'Git Bash', + Pwsh = 'PowerShell' +} + +export interface IBaseUnresolvedTerminalProfile { + args?: string | string[] | undefined; + isAutoDetected?: boolean; + overrideName?: boolean; + icon?: ThemeIcon | URI | { light: URI, dark: URI }; + color?: string; + env?: ITerminalEnvironment; +} + +export interface ITerminalExecutable extends IBaseUnresolvedTerminalProfile { + path: string | string[]; +} + +export interface ITerminalProfileSource extends IBaseUnresolvedTerminalProfile { + source: ProfileSource; +} + +export type ITerminalProfileObject = ITerminalExecutable | ITerminalProfileSource | null; diff --git a/src/vs/platform/terminal/common/terminalEnvironment.ts b/src/vs/platform/terminal/common/terminalEnvironment.ts new file mode 100644 index 0000000000..39b046fc4c --- /dev/null +++ b/src/vs/platform/terminal/common/terminalEnvironment.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ + +export function escapeNonWindowsPath(path: string): string { + let newPath = path; + if (newPath.indexOf('\\') !== 0) { + newPath = newPath.replace(/\\/g, '\\\\'); + } + const bannedChars = /[\`\$\|\&\>\~\#\!\^\*\;\<\"\']/g; + newPath = newPath.replace(bannedChars, ''); + return `'${newPath}'`; +} diff --git a/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts b/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts new file mode 100644 index 0000000000..ef14656d41 --- /dev/null +++ b/src/vs/platform/terminal/common/terminalPlatformConfiguration.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 { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { localize } from 'vs/nls'; +import { ITerminalProfile, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Codicon, iconRegistry } from 'vs/base/common/codicons'; +import { OperatingSystem } from 'vs/base/common/platform'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; + +const terminalProfileBaseProperties: IJSONSchemaMap = { + args: { + description: localize('terminalProfile.args', 'An optional set of arguments to run the shell executable with.'), + type: 'array', + items: { + type: 'string' + } + }, + overrideName: { + description: localize('terminalProfile.overrideName', 'Controls whether or not the profile name overrides the auto detected one.'), + type: 'boolean' + }, + icon: { + description: localize('terminalProfile.icon', 'A codicon ID to associate with this terminal.'), + type: 'string', + enum: Array.from(iconRegistry.all, icon => icon.id), + markdownEnumDescriptions: Array.from(iconRegistry.all, icon => `$(${icon.id})`), + }, + color: { + description: localize('terminalProfile.color', 'A theme color ID to associate with this terminal.'), + type: ['string', 'null'], + enum: [ + 'terminal.ansiBlack', + 'terminal.ansiRed', + 'terminal.ansiGreen', + 'terminal.ansiYellow', + 'terminal.ansiBlue', + 'terminal.ansiMagenta', + 'terminal.ansiCyan', + 'terminal.ansiWhite' + ], + default: null + }, + env: { + markdownDescription: localize('terminalProfile.env', "An object with environment variables that will be added to the terminal profile process. Set to `null` to delete environment variables from the base environment."), + type: 'object', + additionalProperties: { + type: ['string', 'null'] + }, + default: {} + } +}; + +const terminalProfileSchema: IJSONSchema = { + type: 'object', + required: ['path'], + properties: { + path: { + description: localize('terminalProfile.path', 'A single path to a shell executable or an array of paths that will be used as fallbacks when one fails.'), + type: ['string', 'array'], + items: { + type: 'string' + } + }, + ...terminalProfileBaseProperties + } +}; + +const shellDeprecationMessageLinux = localize('terminal.integrated.shell.linux.deprecation', "This is deprecated, the new recommended way to configure your default shell is by creating a terminal profile in {0} and setting its profile name as the default in {1}. This will currently take priority over the new profiles settings but that will change in the future.", '`#terminal.integrated.profiles.linux#`', '`#terminal.integrated.defaultProfile.linux#`'); +const shellDeprecationMessageOsx = localize('terminal.integrated.shell.osx.deprecation', "This is deprecated, the new recommended way to configure your default shell is by creating a terminal profile in {0} and setting its profile name as the default in {1}. This will currently take priority over the new profiles settings but that will change in the future.", '`#terminal.integrated.profiles.osx#`', '`#terminal.integrated.defaultProfile.osx#`'); +const shellDeprecationMessageWindows = localize('terminal.integrated.shell.windows.deprecation', "This is deprecated, the new recommended way to configure your default shell is by creating a terminal profile in {0} and setting its profile name as the default in {1}. This will currently take priority over the new profiles settings but that will change in the future.", '`#terminal.integrated.profiles.windows#`', '`#terminal.integrated.defaultProfile.windows#`'); + +const terminalPlatformConfiguration: IConfigurationNode = { + id: 'terminal', + order: 100, + title: localize('terminalIntegratedConfigurationTitle', "Integrated Terminal"), + type: 'object', + properties: { + [TerminalSettingId.AutomationShellLinux]: { + restricted: true, + markdownDescription: localize({ + key: 'terminal.integrated.automationShell.linux', + comment: ['{0} and {1} are the `shell` and `shellArgs` settings keys'] + }, "A path that when set will override {0} and ignore {1} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.linux`', '`shellArgs`'), + type: ['string', 'null'], + default: null + }, + [TerminalSettingId.AutomationShellMacOs]: { + restricted: true, + markdownDescription: localize({ + key: 'terminal.integrated.automationShell.osx', + comment: ['{0} and {1} are the `shell` and `shellArgs` settings keys'] + }, "A path that when set will override {0} and ignore {1} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.osx`', '`shellArgs`'), + type: ['string', 'null'], + default: null + }, + [TerminalSettingId.AutomationShellWindows]: { + restricted: true, + markdownDescription: localize({ + key: 'terminal.integrated.automationShell.windows', + comment: ['{0} and {1} are the `shell` and `shellArgs` settings keys'] + }, "A path that when set will override {0} and ignore {1} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.windows`', '`shellArgs`'), + type: ['string', 'null'], + default: null + }, + [TerminalSettingId.ShellLinux]: { + restricted: true, + markdownDescription: localize('terminal.integrated.shell.linux', "The path of the shell that the terminal uses on Linux. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + type: ['string', 'null'], + default: null, + markdownDeprecationMessage: shellDeprecationMessageLinux + }, + [TerminalSettingId.ShellMacOs]: { + restricted: true, + markdownDescription: localize('terminal.integrated.shell.osx', "The path of the shell that the terminal uses on macOS. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + type: ['string', 'null'], + default: null, + markdownDeprecationMessage: shellDeprecationMessageOsx + }, + [TerminalSettingId.ShellWindows]: { + restricted: true, + markdownDescription: localize('terminal.integrated.shell.windows', "The path of the shell that the terminal uses on Windows. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + type: ['string', 'null'], + default: null, + markdownDeprecationMessage: shellDeprecationMessageWindows + }, + [TerminalSettingId.ShellArgsLinux]: { + restricted: true, + markdownDescription: localize('terminal.integrated.shellArgs.linux', "The command line arguments to use when on the Linux terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + type: 'array', + items: { + type: 'string' + }, + default: [], + markdownDeprecationMessage: shellDeprecationMessageLinux + }, + [TerminalSettingId.ShellArgsMacOs]: { + restricted: true, + markdownDescription: localize('terminal.integrated.shellArgs.osx', "The command line arguments to use when on the macOS terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + type: 'array', + items: { + type: 'string' + }, + // Unlike on Linux, ~/.profile is not sourced when logging into a macOS session. This + // is the reason terminals on macOS typically run login shells by default which set up + // the environment. See http://unix.stackexchange.com/a/119675/115410 + default: ['-l'], + markdownDeprecationMessage: shellDeprecationMessageOsx + }, + [TerminalSettingId.ShellArgsWindows]: { + restricted: true, + markdownDescription: localize('terminal.integrated.shellArgs.windows', "The command line arguments to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + 'anyOf': [ + { + type: 'array', + items: { + type: 'string', + markdownDescription: localize('terminal.integrated.shellArgs.windows', "The command line arguments to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).") + }, + }, + { + type: 'string', + markdownDescription: localize('terminal.integrated.shellArgs.windows.string', "The command line arguments in [command-line format](https://msdn.microsoft.com/en-au/08dfcab2-eb6e-49a4-80eb-87d4076c98c6) to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).") + } + ], + default: [], + markdownDeprecationMessage: shellDeprecationMessageWindows + }, + [TerminalSettingId.ProfilesWindows]: { + restricted: true, + markdownDescription: localize( + { + key: 'terminal.integrated.profiles.windows', + comment: ['{0}, {1}, and {2} are the `source`, `path` and optional `args` settings keys'] + }, + "The Windows profiles to present when creating a new terminal via the terminal dropdown. Set to null to exclude them, use the {0} property to use the default detected configuration. Or, set the {1} and optional {2}", '`source`', '`path`', '`args`.' + ), + type: 'object', + default: { + 'PowerShell': { + source: 'PowerShell', + icon: 'terminal-powershell' + }, + 'Command Prompt': { + path: [ + '${env:windir}\\Sysnative\\cmd.exe', + '${env:windir}\\System32\\cmd.exe' + ], + args: [], + icon: 'terminal-cmd' + }, + 'Git Bash': { + source: 'Git Bash' + } + }, + additionalProperties: { + 'anyOf': [ + { + type: 'object', + required: ['source'], + properties: { + source: { + description: localize('terminalProfile.windowsSource', 'A profile source that will auto detect the paths to the shell.'), + enum: ['PowerShell', 'Git Bash'] + }, + ...terminalProfileBaseProperties + } + }, + { type: 'null' }, + terminalProfileSchema + ] + } + }, + [TerminalSettingId.ProfilesMacOs]: { + restricted: true, + markdownDescription: localize( + { + key: 'terminal.integrated.profile.osx', + comment: ['{0} and {1} are the `path` and optional `args` settings keys'] + }, + "The macOS profiles to present when creating a new terminal via the terminal dropdown. When set, these will override the default detected profiles. They are comprised of a {0} and optional {1}", '`path`', '`args`.' + ), + type: 'object', + default: { + 'bash': { + path: 'bash', + args: ['-l'], + icon: 'terminal-bash' + }, + 'zsh': { + path: 'zsh', + args: ['-l'] + }, + 'fish': { + path: 'fish', + args: ['-l'] + }, + 'tmux': { + path: 'tmux', + icon: 'terminal-tmux' + }, + 'pwsh': { + path: 'pwsh', + icon: 'terminal-powershell' + } + }, + additionalProperties: { + 'anyOf': [ + { type: 'null' }, + terminalProfileSchema + ] + } + }, + [TerminalSettingId.ProfilesLinux]: { + restricted: true, + markdownDescription: localize( + { + key: 'terminal.integrated.profile.linux', + comment: ['{0} and {1} are the `path` and optional `args` settings keys'] + }, + "The Linux profiles to present when creating a new terminal via the terminal dropdown. When set, these will override the default detected profiles. They are comprised of a {0} and optional {1}", '`path`', '`args`.' + ), + type: 'object', + default: { + 'bash': { + path: 'bash' + }, + 'zsh': { + path: 'zsh' + }, + 'fish': { + path: 'fish' + }, + 'tmux': { + path: 'tmux', + icon: 'terminal-tmux' + }, + 'pwsh': { + path: 'pwsh', + icon: 'terminal-powershell' + } + }, + additionalProperties: { + 'anyOf': [ + { type: 'null' }, + terminalProfileSchema + ] + } + }, + [TerminalSettingId.UseWslProfiles]: { + description: localize('terminal.integrated.useWslProfiles', 'Controls whether or not WSL distros are shown in the terminal dropdown'), + type: 'boolean', + default: true + }, + [TerminalSettingId.InheritEnv]: { + scope: ConfigurationScope.APPLICATION, + description: localize('terminal.integrated.inheritEnv', "Whether new shells should inherit their environment from VS Code which may source a login shell to ensure $PATH and other development variables are initialized. This has no effect on Windows."), + type: 'boolean', + default: true + }, + [TerminalSettingId.ShowLinkHover]: { + scope: ConfigurationScope.APPLICATION, + description: localize('terminal.integrated.showLinkHover', "Whether to show hovers for links in the terminal output."), + type: 'boolean', + default: true + } + } +}; + +/** + * Registers terminal configurations required by shared process and remote server. + */ +export function registerTerminalPlatformConfiguration() { + Registry.as(Extensions.Configuration).registerConfiguration(terminalPlatformConfiguration); + registerTerminalDefaultProfileConfiguration(); +} + +let lastDefaultProfilesConfiguration: IConfigurationNode | undefined; +export function registerTerminalDefaultProfileConfiguration(detectedProfiles?: { os: OperatingSystem, profiles: ITerminalProfile[] }) { + const registry = Registry.as(Extensions.Configuration); + if (lastDefaultProfilesConfiguration) { + registry.deregisterConfigurations([lastDefaultProfilesConfiguration]); + } + let enumValues: string[] | undefined = undefined; + let enumDescriptions: string[] | undefined = undefined; + if (detectedProfiles) { + const result = detectedProfiles.profiles.map(e => { + return { + name: e.profileName, + description: createProfileDescription(e) + }; + }); + enumValues = result.map(e => e.name); + enumDescriptions = result.map(e => e.description); + } + lastDefaultProfilesConfiguration = { + id: 'terminal', + order: 100, + title: localize('terminalIntegratedConfigurationTitle', "Integrated Terminal"), + type: 'object', + properties: { + [TerminalSettingId.DefaultProfileLinux]: { + restricted: true, + markdownDescription: localize('terminal.integrated.defaultProfile.linux', "The default profile used on Linux. This setting will currently be ignored if either {0} or {1} are set.", '`#terminal.integrated.shell.linux#`', '`#terminal.integrated.shellArgs.linux#`'), + type: ['string', 'null'], + default: null, + enum: detectedProfiles?.os === OperatingSystem.Linux ? enumValues : undefined, + markdownEnumDescriptions: detectedProfiles?.os === OperatingSystem.Linux ? enumDescriptions : undefined + }, + [TerminalSettingId.DefaultProfileMacOs]: { + restricted: true, + markdownDescription: localize('terminal.integrated.defaultProfile.osx', "The default profile used on macOS. This setting will currently be ignored if either {0} or {1} are set.", '`#terminal.integrated.shell.osx#`', '`#terminal.integrated.shellArgs.osx#`'), + type: ['string', 'null'], + default: null, + enum: detectedProfiles?.os === OperatingSystem.Macintosh ? enumValues : undefined, + markdownEnumDescriptions: detectedProfiles?.os === OperatingSystem.Macintosh ? enumDescriptions : undefined + }, + [TerminalSettingId.DefaultProfileWindows]: { + restricted: true, + markdownDescription: localize('terminal.integrated.defaultProfile.windows', "The default profile used on Windows. This setting will currently be ignored if either {0} or {1} are set.", '`#terminal.integrated.shell.windows#`', '`#terminal.integrated.shellArgs.windows#`'), + type: ['string', 'null'], + default: null, + enum: detectedProfiles?.os === OperatingSystem.Windows ? enumValues : undefined, + markdownEnumDescriptions: detectedProfiles?.os === OperatingSystem.Windows ? enumDescriptions : undefined + }, + } + }; + registry.registerConfiguration(lastDefaultProfilesConfiguration); +} + +function createProfileDescription(profile: ITerminalProfile): string { + let description = `$(${ThemeIcon.isThemeIcon(profile.icon) ? profile.icon.id : profile.icon ? profile.icon : Codicon.terminal.id}) ${profile.profileName}\n- path: ${profile.path}`; + if (profile.args) { + if (typeof profile.args === 'string') { + description += `\n- args: "${profile.args}"`; + } else { + description += `\n- args: [${profile.args.length === 0 ? '' : profile.args.join(`','`)}]`; + } + } + if (profile.overrideName !== undefined) { + description += `\n- overrideName: ${profile.overrideName}`; + } + if (profile.color) { + description += `\n- color: ${profile.color}`; + } + if (profile.env) { + description += `\n- env: ${JSON.stringify(profile.env)}`; + } + return description; +} diff --git a/src/vs/platform/terminal/common/terminalProcess.ts b/src/vs/platform/terminal/common/terminalProcess.ts index 6cdf3b88e1..9a00edff97 100644 --- a/src/vs/platform/terminal/common/terminalProcess.ts +++ b/src/vs/platform/terminal/common/terminalProcess.ts @@ -1,10 +1,10 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { UriComponents } from 'vs/base/common/uri'; -import { IRawTerminalTabLayoutInfo, ITerminalEnvironment, ITerminalTabLayoutInfoById } from 'vs/platform/terminal/common/terminal'; +import { IRawTerminalTabLayoutInfo, ITerminalEnvironment, ITerminalTabLayoutInfoById, TerminalIcon, TitleEventSource } from 'vs/platform/terminal/common/terminal'; import { ISerializableEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable'; export interface ISingleTerminalConfiguration { @@ -26,7 +26,6 @@ export interface ICompleteTerminalConfiguration { 'terminal.integrated.env.windows': ISingleTerminalConfiguration; 'terminal.integrated.env.osx': ISingleTerminalConfiguration; 'terminal.integrated.env.linux': ISingleTerminalConfiguration; - 'terminal.integrated.inheritEnv': boolean; 'terminal.integrated.cwd': string; 'terminal.integrated.detectLocale': 'auto' | 'off' | 'on'; } @@ -52,11 +51,13 @@ export interface IProcessDetails { id: number; pid: number; title: string; + titleSource: TitleEventSource; cwd: string; workspaceId: string; workspaceName: string; isOrphan: boolean; - icon: string | undefined; + icon: TerminalIcon | undefined; + color: string | undefined; } export type ITerminalTabLayoutInfoDto = IRawTerminalTabLayoutInfo; diff --git a/src/vs/platform/terminal/common/terminalRecorder.ts b/src/vs/platform/terminal/common/terminalRecorder.ts index e8a6e17c36..00d22dec06 100644 --- a/src/vs/platform/terminal/common/terminalRecorder.ts +++ b/src/vs/platform/terminal/common/terminalRecorder.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IPtyHostProcessReplayEvent, ReplayEntry } from 'vs/platform/terminal/common/terminalProcess'; @@ -26,7 +26,7 @@ export class TerminalRecorder { this._entries = [{ cols, rows, data: [] }]; } - public recordResize(cols: number, rows: number): void { + recordResize(cols: number, rows: number): void { if (this._entries.length > 0) { const lastEntry = this._entries[this._entries.length - 1]; if (lastEntry.data.length === 0) { @@ -52,7 +52,7 @@ export class TerminalRecorder { this._entries.push({ cols, rows, data: [] }); } - public recordData(data: string): void { + recordData(data: string): void { const lastEntry = this._entries[this._entries.length - 1]; lastEntry.data.push(data); @@ -76,7 +76,7 @@ export class TerminalRecorder { } } - public generateReplayEvent(): IPtyHostProcessReplayEvent { + generateReplayEvent(): IPtyHostProcessReplayEvent { // normalize entries to one element per data array this._entries.forEach((entry) => { if (entry.data.length > 0) { diff --git a/src/vs/platform/terminal/electron-sandbox/terminal.ts b/src/vs/platform/terminal/electron-sandbox/terminal.ts index a493743cde..b549f6af4b 100644 --- a/src/vs/platform/terminal/electron-sandbox/terminal.ts +++ b/src/vs/platform/terminal/electron-sandbox/terminal.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/platform/terminal/node/heartbeatService.ts b/src/vs/platform/terminal/node/heartbeatService.ts index 3a414958af..60ccee91a7 100644 --- a/src/vs/platform/terminal/node/heartbeatService.ts +++ b/src/vs/platform/terminal/node/heartbeatService.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Emitter } from 'vs/base/common/event'; diff --git a/src/vs/platform/terminal/node/ptyHostMain.ts b/src/vs/platform/terminal/node/ptyHostMain.ts index d3b08384ec..738cbad094 100644 --- a/src/vs/platform/terminal/node/ptyHostMain.ts +++ b/src/vs/platform/terminal/node/ptyHostMain.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Server } from 'vs/base/parts/ipc/node/ipc.cp'; @@ -23,7 +23,11 @@ server.registerChannel(TerminalIpcChannels.Log, logChannel); const heartbeatService = new HeartbeatService(); server.registerChannel(TerminalIpcChannels.Heartbeat, ProxyChannel.fromService(heartbeatService)); -const ptyService = new PtyService(lastPtyId, logService); +const reconnectConstants = { GraceTime: parseInt(process.env.VSCODE_RECONNECT_GRACE_TIME || '0'), ShortGraceTime: parseInt(process.env.VSCODE_RECONNECT_SHORT_GRACE_TIME || '0') }; +delete process.env.VSCODE_RECONNECT_GRACE_TIME; +delete process.env.VSCODE_RECONNECT_SHORT_GRACE_TIME; + +const ptyService = new PtyService(lastPtyId, logService, reconnectConstants); server.registerChannel(TerminalIpcChannels.PtyHost, ProxyChannel.fromService(ptyService)); process.once('exit', () => { diff --git a/src/vs/platform/terminal/node/ptyHostService.ts b/src/vs/platform/terminal/node/ptyHostService.ts index fba6308c4e..7d616db003 100644 --- a/src/vs/platform/terminal/node/ptyHostService.ts +++ b/src/vs/platform/terminal/node/ptyHostService.ts @@ -1,11 +1,11 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; -import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalsLayoutInfo, TerminalIpcChannels, IHeartbeatService, HeartbeatConstants, TerminalShellType } from 'vs/platform/terminal/common/terminal'; +import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalsLayoutInfo, TerminalIpcChannels, IHeartbeatService, HeartbeatConstants, TerminalShellType, ITerminalProfile, IRequestResolveVariablesEvent, TitleEventSource, TerminalIcon, IReconnectConstants } from 'vs/platform/terminal/common/terminal'; import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; import { FileAccess } from 'vs/base/common/network'; import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; @@ -14,6 +14,9 @@ import { Emitter } from 'vs/base/common/event'; import { LogLevelChannelClient } from 'vs/platform/log/common/logIpc'; import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { detectAvailableProfiles } from 'vs/platform/terminal/node/terminalProfiles'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { registerTerminalPlatformConfiguration } from 'vs/platform/terminal/common/terminalPlatformConfiguration'; enum Constants { MaxRestarts = 5 @@ -25,6 +28,8 @@ enum Constants { */ let lastPtyId = 0; +let lastResolveVariablesRequestId = 0; + /** * This service implements IPtyService by launching a pty host process, forwarding messages to and * from the pty host process and manages the connection. @@ -51,6 +56,9 @@ export class PtyHostService extends Disposable implements IPtyService { readonly onPtyHostUnresponsive = this._onPtyHostUnresponsive.event; private readonly _onPtyHostResponsive = this._register(new Emitter()); readonly onPtyHostResponsive = this._onPtyHostResponsive.event; + private readonly _onPtyHostRequestResolveVariables = this._register(new Emitter()); + readonly onPtyHostRequestResolveVariables = this._onPtyHostRequestResolveVariables.event; + private readonly _onProcessData = this._register(new Emitter<{ id: number, event: IProcessDataEvent | string }>()); readonly onProcessData = this._onProcessData.event; private readonly _onProcessExit = this._register(new Emitter<{ id: number, event: number | undefined }>()); @@ -71,11 +79,17 @@ export class PtyHostService extends Disposable implements IPtyService { readonly onProcessOrphanQuestion = this._onProcessOrphanQuestion.event; constructor( + private readonly _reconnectConstants: IReconnectConstants, + @IConfigurationService private readonly _configurationService: IConfigurationService, @ILogService private readonly _logService: ILogService, @ITelemetryService private readonly _telemetryService: ITelemetryService ) { super(); + // Platform configuration is required on the process running the pty host (shared process or + // remote server). + registerTerminalPlatformConfiguration(); + this._register(toDisposable(() => this._disposePtyHost())); [this._client, this._proxy] = this._startPtyHost(); @@ -91,7 +105,9 @@ export class PtyHostService extends Disposable implements IPtyService { VSCODE_LAST_PTY_ID: lastPtyId, VSCODE_AMD_ENTRYPOINT: 'vs/platform/terminal/node/ptyHostMain', VSCODE_PIPE_LOGGING: 'true', - VSCODE_VERBOSE_LOGGING: 'true' // transmit console logs from server to client + VSCODE_VERBOSE_LOGGING: 'true', // transmit console logs from server to client, + VSCODE_RECONNECT_GRACE_TIME: this._reconnectConstants.GraceTime, + VSCODE_RECONNECT_SHORT_GRACE_TIME: this._reconnectConstants.ShortGraceTime } } ); @@ -122,6 +138,7 @@ export class PtyHostService extends Disposable implements IPtyService { // Setup logging const logChannel = client.getChannel(TerminalIpcChannels.Log); + LogLevelChannelClient.setLevel(logChannel, this._logService.getLevel()); this._register(this._logService.onDidChangeLogLevel(() => { LogLevelChannelClient.setLevel(logChannel, this._logService.getLevel()); })); @@ -153,6 +170,12 @@ export class PtyHostService extends Disposable implements IPtyService { lastPtyId = Math.max(lastPtyId, id); return id; } + updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise { + return this._proxy.updateTitle(id, title, titleSource); + } + updateIcon(id: number, icon: TerminalIcon, color?: string): Promise { + return this._proxy.updateIcon(id, icon, color); + } attachToProcess(id: number): Promise { return this._proxy.attachToProcess(id); } @@ -199,8 +222,14 @@ export class PtyHostService extends Disposable implements IPtyService { getDefaultSystemShell(osOverride?: OperatingSystem): Promise { return this._proxy.getDefaultSystemShell(osOverride); } - getShellEnvironment(): Promise { - return this._proxy.getShellEnvironment(); + async getProfiles(profiles: unknown, defaultProfile: unknown, includeDetectedProfiles: boolean = false): Promise { + return detectAvailableProfiles(profiles, defaultProfile, includeDetectedProfiles, this._configurationService, undefined, this._logService, this._resolveVariables.bind(this)); + } + getEnvironment(): Promise { + return this._proxy.getEnvironment(); + } + getWslPath(original: string): Promise { + return this._proxy.getWslPath(original); } setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise { @@ -279,4 +308,22 @@ export class PtyHostService extends Disposable implements IPtyService { this._heartbeatSecondTimeout = undefined; } } + + private _pendingResolveVariablesRequests: Map void> = new Map(); + private _resolveVariables(text: string[]): Promise { + return new Promise(resolve => { + const id = ++lastResolveVariablesRequestId; + this._pendingResolveVariablesRequests.set(id, resolve); + this._onPtyHostRequestResolveVariables.fire({ id, originalText: text }); + }); + } + async acceptPtyHostResolvedVariables(id: number, resolved: string[]) { + const request = this._pendingResolveVariablesRequests.get(id); + if (request) { + request(resolved); + this._pendingResolveVariablesRequests.delete(id); + } else { + this._logService.warn(`Resolved variables received without matching request ${id}`); + } + } } diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index d9ea875c40..9a54097c84 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -1,11 +1,11 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IProcessEnvironment, OperatingSystem, OS } from 'vs/base/common/platform'; -import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, LocalReconnectConstants, ITerminalsLayoutInfo, IRawTerminalInstanceLayoutInfo, ITerminalTabLayoutInfoById, ITerminalInstanceLayoutInfoById, TerminalShellType } from 'vs/platform/terminal/common/terminal'; +import { IProcessEnvironment, isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; +import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalsLayoutInfo, IRawTerminalInstanceLayoutInfo, ITerminalTabLayoutInfoById, ITerminalInstanceLayoutInfoById, TerminalShellType, IProcessReadyEvent, TitleEventSource, TerminalIcon, IReconnectConstants } from 'vs/platform/terminal/common/terminal'; import { AutoOpenBarrier, Queue, RunOnceScheduler } from 'vs/base/common/async'; import { Emitter } from 'vs/base/common/event'; import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder'; @@ -14,6 +14,10 @@ import { ISetTerminalLayoutInfoArgs, ITerminalTabLayoutInfoDto, IProcessDetails, import { ILogService } from 'vs/platform/log/common/log'; import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; import { getSystemShell } from 'vs/base/node/shell'; +import { getWindowsBuildNumber } from 'vs/platform/terminal/node/terminalEnvironment'; +import { execFile } from 'child_process'; +import { escapeNonWindowsPath } from 'vs/platform/terminal/common/terminalEnvironment'; +import { URI } from 'vs/base/common/uri'; type WorkspaceId = string; @@ -47,7 +51,8 @@ export class PtyService extends Disposable implements IPtyService { constructor( private _lastPtyId: number, - private readonly _logService: ILogService + private readonly _logService: ILogService, + private readonly _reconnectConstants: IReconnectConstants ) { super(); @@ -88,7 +93,7 @@ export class PtyService extends Disposable implements IPtyService { if (process.onProcessResolvedShellLaunchConfig) { process.onProcessResolvedShellLaunchConfig(event => this._onProcessResolvedShellLaunchConfig.fire({ id, event })); } - const persistentProcess = new PersistentTerminalProcess(id, process, workspaceId, workspaceName, shouldPersist, cols, rows, this._logService, shellLaunchConfig.icon); + const persistentProcess = new PersistentTerminalProcess(id, process, workspaceId, workspaceName, shouldPersist, cols, rows, this._reconnectConstants, this._logService, shellLaunchConfig.icon); process.onProcessExit(() => { persistentProcess.dispose(); this._ptys.delete(id); @@ -111,6 +116,14 @@ export class PtyService extends Disposable implements IPtyService { } } + async updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise { + this._throwIfNoPty(id).setTitle(title, titleSource); + } + + async updateIcon(id: number, icon: URI | { light: URI; dark: URI } | { id: string, color?: { id: string } }, color?: string): Promise { + this._throwIfNoPty(id).setIcon(icon, color); + } + async detachFromProcess(id: number): Promise { this._throwIfNoPty(id).detach(); } @@ -166,10 +179,25 @@ export class PtyService extends Disposable implements IPtyService { return getSystemShell(osOverride, process.env); } - async getShellEnvironment(): Promise { + async getEnvironment(): Promise { return { ...process.env }; } + async getWslPath(original: string): Promise { + if (!isWindows) { + return original; + } + if (getWindowsBuildNumber() < 17063) { + return original.replace(/\\/g, '/'); + } + return new Promise(c => { + const proc = execFile('bash.exe', ['-c', `wslpath ${escapeNonWindowsPath(original)}`], {}, (error, stdout, stderr) => { + c(escapeNonWindowsPath(stdout.trim())); + }); + proc.stdin!.end(); + }); + } + async setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise { this._workspaceLayoutInfos.set(args.workspaceId, args); } @@ -178,10 +206,8 @@ export class PtyService extends Disposable implements IPtyService { const layout = this._workspaceLayoutInfos.get(args.workspaceId); if (layout) { const expandedTabs = await Promise.all(layout.tabs.map(async tab => this._expandTerminalTab(tab))); - const filtered = expandedTabs.filter(t => t.terminals.length > 0); - return { - tabs: filtered - }; + const tabs = expandedTabs.filter(t => t.terminals.length > 0); + return { tabs }; } return undefined; } @@ -219,12 +245,14 @@ export class PtyService extends Disposable implements IPtyService { return { id, title: persistentProcess.title, + titleSource: persistentProcess.titleSource, pid: persistentProcess.pid, workspaceId: persistentProcess.workspaceId, workspaceName: persistentProcess.workspaceName, cwd, isOrphan, - icon: persistentProcess.icon + icon: persistentProcess.icon, + color: persistentProcess.color }; } @@ -254,7 +282,7 @@ export class PersistentTerminalProcess extends Disposable { private readonly _onProcessReplay = this._register(new Emitter()); readonly onProcessReplay = this._onProcessReplay.event; - private readonly _onProcessReady = this._register(new Emitter<{ pid: number, cwd: string }>()); + private readonly _onProcessReady = this._register(new Emitter()); readonly onProcessReady = this._onProcessReady.event; private readonly _onProcessTitleChanged = this._register(new Emitter()); readonly onProcessTitleChanged = this._onProcessTitleChanged.event; @@ -271,33 +299,49 @@ export class PersistentTerminalProcess extends Disposable { private _pid = -1; private _cwd = ''; + private _title: string | undefined; + private _titleSource: TitleEventSource = TitleEventSource.Process; get pid(): number { return this._pid; } - get title(): string { return this._terminalProcess.currentTitle; } - get icon(): string | undefined { return this._icon; } + get title(): string { return this._title || this._terminalProcess.currentTitle; } + get titleSource(): TitleEventSource { return this._titleSource; } + get icon(): TerminalIcon | undefined { return this._icon; } + get color(): string | undefined { return this._color; } + + setTitle(title: string, titleSource: TitleEventSource): void { + this._title = title; + this._titleSource = titleSource; + } + + setIcon(icon: TerminalIcon, color?: string): void { + this._icon = icon; + this._color = color; + } constructor( private _persistentProcessId: number, private readonly _terminalProcess: TerminalProcess, - public readonly workspaceId: string, - public readonly workspaceName: string, - public readonly shouldPersistTerminal: boolean, + readonly workspaceId: string, + readonly workspaceName: string, + readonly shouldPersistTerminal: boolean, cols: number, rows: number, + reconnectConstants: IReconnectConstants, private readonly _logService: ILogService, - private readonly _icon?: string + private _icon?: TerminalIcon, + private _color?: string ) { super(); this._recorder = new TerminalRecorder(cols, rows); this._orphanQuestionBarrier = null; this._orphanQuestionReplyTime = 0; this._disconnectRunner1 = this._register(new RunOnceScheduler(() => { - this._logService.info(`Persistent process "${this._persistentProcessId}": The reconnection grace time of ${printTime(LocalReconnectConstants.ReconnectionGraceTime)} has expired, shutting down pid "${this._pid}"`); + this._logService.info(`Persistent process "${this._persistentProcessId}": The reconnection grace time of ${printTime(reconnectConstants.GraceTime)} has expired, shutting down pid "${this._pid}"`); this.shutdown(true); - }, LocalReconnectConstants.ReconnectionGraceTime)); + }, reconnectConstants.GraceTime)); this._disconnectRunner2 = this._register(new RunOnceScheduler(() => { - this._logService.info(`Persistent process "${this._persistentProcessId}": The short reconnection grace time of ${printTime(LocalReconnectConstants.ReconnectionShortGraceTime)} has expired, shutting down pid ${this._pid}`); + this._logService.info(`Persistent process "${this._persistentProcessId}": The short reconnection grace time of ${printTime(reconnectConstants.ShortGraceTime)} has expired, shutting down pid ${this._pid}`); this.shutdown(true); - }, LocalReconnectConstants.ReconnectionShortGraceTime)); + }, reconnectConstants.ShortGraceTime)); this._register(this._terminalProcess.onProcessReady(e => { this._pid = e.pid; @@ -337,7 +381,7 @@ export class PersistentTerminalProcess extends Disposable { } this._isStarted = true; } else { - this._onProcessReady.fire({ pid: this._pid, cwd: this._cwd }); + this._onProcessReady.fire({ pid: this._pid, cwd: this._cwd, requiresWindowsMode: isWindows && getWindowsBuildNumber() < 21376 }); this._onProcessTitleChanged.fire(this._terminalProcess.currentTitle); this._onProcessShellTypeChanged.fire(this._terminalProcess.shellType); this.triggerReplay(); diff --git a/src/vs/platform/terminal/node/terminalEnvironment.ts b/src/vs/platform/terminal/node/terminalEnvironment.ts index 91f40c9d80..5cdd200ca3 100644 --- a/src/vs/platform/terminal/node/terminalEnvironment.ts +++ b/src/vs/platform/terminal/node/terminalEnvironment.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as os from 'os'; @@ -20,7 +20,7 @@ export function getWindowsBuildNumber(): number { return buildNumber; } -export async function findExecutable(command: string, cwd?: string, paths?: string[], env: IProcessEnvironment = process.env as IProcessEnvironment, exists: (path: string) => Promise = pfs.exists): Promise { +export async function findExecutable(command: string, cwd?: string, paths?: string[], env: IProcessEnvironment = process.env as IProcessEnvironment, exists: (path: string) => Promise = pfs.Promises.exists): Promise { // If we have an absolute path then we take it. if (path.isAbsolute(command)) { return await exists(command) ? command : undefined; diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index 32db8966b8..4215fc1eb9 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -5,11 +5,10 @@ import * as path from 'vs/base/common/path'; import type * as pty from 'node-pty'; -import * as fs from 'fs'; import * as os from 'os'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IShellLaunchConfig, ITerminalLaunchError, FlowControlConstants, ITerminalChildProcess, ITerminalDimensionsOverride, TerminalShellType } from 'vs/platform/terminal/common/terminal'; +import { IShellLaunchConfig, ITerminalLaunchError, FlowControlConstants, ITerminalChildProcess, ITerminalDimensionsOverride, TerminalShellType, IProcessReadyEvent } from 'vs/platform/terminal/common/terminal'; import { exec } from 'child_process'; import { ILogService } from 'vs/platform/log/common/log'; import { findExecutable, getWindowsBuildNumber } from 'vs/platform/terminal/node/terminalEnvironment'; @@ -18,6 +17,7 @@ import { localize } from 'vs/nls'; import { WindowsShellHelper } from 'vs/platform/terminal/node/windowsShellHelper'; import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { timeout } from 'vs/base/common/async'; +import { Promises } from 'vs/base/node/pfs'; // Writing large amounts of data can be corrupted for some reason, after looking into this is // appears to be a race condition around writing to the FD which may be based on how powerful the @@ -90,21 +90,21 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess private _isPtyPaused: boolean = false; private _unacknowledgedCharCount: number = 0; - public get exitMessage(): string | undefined { return this._exitMessage; } + get exitMessage(): string | undefined { return this._exitMessage; } - public get currentTitle(): string { return this._windowsShellHelper?.shellTitle || this._currentTitle; } - public get shellType(): TerminalShellType { return this._windowsShellHelper ? this._windowsShellHelper.shellType : undefined; } + get currentTitle(): string { return this._windowsShellHelper?.shellTitle || this._currentTitle; } + get shellType(): TerminalShellType { return this._windowsShellHelper ? this._windowsShellHelper.shellType : undefined; } private readonly _onProcessData = this._register(new Emitter()); - public get onProcessData(): Event { return this._onProcessData.event; } + get onProcessData(): Event { return this._onProcessData.event; } private readonly _onProcessExit = this._register(new Emitter()); - public get onProcessExit(): Event { return this._onProcessExit.event; } - private readonly _onProcessReady = this._register(new Emitter<{ pid: number, cwd: string }>()); - public get onProcessReady(): Event<{ pid: number, cwd: string }> { return this._onProcessReady.event; } + get onProcessExit(): Event { return this._onProcessExit.event; } + private readonly _onProcessReady = this._register(new Emitter()); + get onProcessReady(): Event { return this._onProcessReady.event; } private readonly _onProcessTitleChanged = this._register(new Emitter()); - public get onProcessTitleChanged(): Event { return this._onProcessTitleChanged.event; } + get onProcessTitleChanged(): Event { return this._onProcessTitleChanged.event; } private readonly _onProcessShellTypeChanged = this._register(new Emitter()); - public readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; + readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; constructor( private readonly _shellLaunchConfig: IShellLaunchConfig, @@ -164,7 +164,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess onProcessOverrideDimensions?: Event | undefined; onProcessResolvedShellLaunchConfig?: Event | undefined; - public async start(): Promise { + async start(): Promise { const results = await Promise.all([this._validateCwd(), this._validateExecutable()]); const firstError = results.find(r => r !== undefined); if (firstError) { @@ -182,7 +182,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess private async _validateCwd(): Promise { try { - const result = await fs.promises.stat(this._initialCwd); + const result = await Promises.stat(this._initialCwd); if (!result.isDirectory()) { return { message: localize('launchFail.cwdNotDirectory', "Starting directory (cwd) \"{0}\" is not a directory", this._initialCwd.toString()) }; } @@ -200,7 +200,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess throw new Error('IShellLaunchConfig.executable not set'); } try { - const result = await fs.promises.stat(slc.executable); + const result = await Promises.stat(slc.executable); if (!result.isFile() && !result.isSymbolicLink()) { return { message: localize('launchFail.executableIsNotFileOrSymlink', "Path to shell executable \"{0}\" is not a file of a symlink", slc.executable) }; } @@ -239,7 +239,6 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess ptyProcess.pause(); } - // Refire the data event this._onProcessData.fire(data); if (this._closeTimeout) { @@ -251,11 +250,11 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess this._exitCode = e.exitCode; this._queueProcessExit(); }); - this._setupTitlePolling(ptyProcess); this._sendProcessId(ptyProcess.pid); + this._setupTitlePolling(ptyProcess); } - public override dispose(): void { + override dispose(): void { this._isDisposed = true; if (this._titleInterval) { clearInterval(this._titleInterval); @@ -266,7 +265,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess private _setupTitlePolling(ptyProcess: pty.IPty) { // Send initial timeout async to give event listeners a chance to init - setTimeout(() => this._sendProcessTitle(ptyProcess), 0); + setTimeout(() => this._sendProcessTitle(ptyProcess)); // Setup polling for non-Windows, for Windows `process` doesn't change if (!isWindows) { this._titleInterval = setInterval(() => { @@ -325,7 +324,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } private _sendProcessId(pid: number) { - this._onProcessReady.fire({ pid, cwd: this._initialCwd }); + this._onProcessReady.fire({ pid, cwd: this._initialCwd, requiresWindowsMode: isWindows && getWindowsBuildNumber() < 21376 }); } private _sendProcessTitle(ptyProcess: pty.IPty): void { @@ -336,7 +335,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess this._onProcessTitleChanged.fire(this._currentTitle); } - public shutdown(immediate: boolean): void { + shutdown(immediate: boolean): void { // don't force immediate disposal of the terminal processes on Windows as an additional // mitigation for https://github.com/microsoft/vscode/issues/71966 which causes the pty host // to become unresponsive, disconnecting all terminals across all windows. @@ -356,7 +355,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } } - public input(data: string, isBinary?: boolean): void { + input(data: string, isBinary?: boolean): void { if (this._isDisposed || !this._ptyProcess) { return; } @@ -370,7 +369,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess this._startWrite(); } - public async processBinary(data: string): Promise { + async processBinary(data: string): Promise { this.input(data, true); } @@ -404,7 +403,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } } - public resize(cols: number, rows: number): void { + resize(cols: number, rows: number): void { if (this._isDisposed) { return; } @@ -437,7 +436,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } } - public acknowledgeDataEvent(charCount: number): void { + acknowledgeDataEvent(charCount: number): void { // Prevent lower than 0 to heal from errors this._unacknowledgedCharCount = Math.max(this._unacknowledgedCharCount - charCount, 0); this._logService.trace(`Flow control: Ack ${charCount} chars (unacknowledged: ${this._unacknowledgedCharCount})`); @@ -448,7 +447,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } } - public clearUnacknowledgedChars(): void { + clearUnacknowledgedChars(): void { this._unacknowledgedCharCount = 0; this._logService.trace(`Flow control: Cleared all unacknowledged chars, forcing resume`); if (this._isPtyPaused) { @@ -457,11 +456,11 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } } - public getInitialCwd(): Promise { + getInitialCwd(): Promise { return Promise.resolve(this._initialCwd); } - public getCwd(): Promise { + async getCwd(): Promise { if (isMacintosh) { // Disable cwd lookup on macOS Big Sur due to spawn blocking thread (darwin v20 is macOS // Big Sur) https://github.com/Microsoft/vscode/issues/105446 @@ -486,27 +485,21 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } if (isLinux) { - return new Promise(resolve => { - if (!this._ptyProcess) { - resolve(this._initialCwd); - return; - } - this._logService.trace('IPty#pid'); - fs.readlink('/proc/' + this._ptyProcess.pid + '/cwd', (err, linkedstr) => { - if (err) { - resolve(this._initialCwd); - } - resolve(linkedstr); - }); - }); + if (!this._ptyProcess) { + return this._initialCwd; + } + this._logService.trace('IPty#pid'); + try { + return await Promises.readlink(`/proc/${this._ptyProcess.pid}/cwd`); + } catch (error) { + return this._initialCwd; + } } - return new Promise(resolve => { - resolve(this._initialCwd); - }); + return this._initialCwd; } - public getLatency(): Promise { + getLatency(): Promise { return Promise.resolve(0); } } @@ -515,12 +508,12 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess * Tracks the latest resize event to be trigger at a later point. */ class DelayedResizer extends Disposable { - public rows: number | undefined; - public cols: number | undefined; + rows: number | undefined; + cols: number | undefined; private _timeout: NodeJS.Timeout; private readonly _onTrigger = this._register(new Emitter<{ rows?: number, cols?: number }>()); - public get onTrigger(): Event<{ rows?: number, cols?: number }> { return this._onTrigger.event; } + get onTrigger(): Event<{ rows?: number, cols?: number }> { return this._onTrigger.event; } constructor() { super(); diff --git a/src/vs/platform/terminal/node/terminalProfiles.ts b/src/vs/platform/terminal/node/terminalProfiles.ts new file mode 100644 index 0000000000..22b460b5b1 --- /dev/null +++ b/src/vs/platform/terminal/node/terminalProfiles.ts @@ -0,0 +1,362 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { normalize, basename, delimiter } from 'vs/base/common/path'; +import { enumeratePowerShellInstallations } from 'vs/base/node/powershell'; +import { findExecutable, getWindowsBuildNumber } from 'vs/platform/terminal/node/terminalEnvironment'; +import * as cp from 'child_process'; +import { ILogService } from 'vs/platform/log/common/log'; +import * as pfs from 'vs/base/node/pfs'; +import { ITerminalEnvironment, ITerminalProfile, ITerminalProfileObject, ProfileSource, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { Codicon } from 'vs/base/common/codicons'; +import { isLinux, isWindows } from 'vs/base/common/platform'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { URI } from 'vs/base/common/uri'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + +let profileSources: Map | undefined; + +export function detectAvailableProfiles( + profiles: unknown, + defaultProfile: unknown, + includeDetectedProfiles: boolean, + configurationService: IConfigurationService, + fsProvider?: IFsProvider, + logService?: ILogService, + variableResolver?: (text: string[]) => Promise, + testPwshSourcePaths?: string[] +): Promise { + fsProvider = fsProvider || { + existsFile: pfs.SymlinkSupport.existsFile, + readFile: pfs.Promises.readFile + }; + if (isWindows) { + return detectAvailableWindowsProfiles( + includeDetectedProfiles, + fsProvider, + logService, + configurationService.getValue(TerminalSettingId.UseWslProfiles) !== false, + profiles && typeof profiles === 'object' ? { ...profiles } : configurationService.getValue<{ [key: string]: ITerminalProfileObject }>(TerminalSettingId.ProfilesWindows), + typeof defaultProfile === 'string' ? defaultProfile : configurationService.getValue(TerminalSettingId.DefaultProfileWindows), + testPwshSourcePaths, + variableResolver + ); + } + return detectAvailableUnixProfiles( + fsProvider, + logService, + includeDetectedProfiles, + profiles && typeof profiles === 'object' ? { ...profiles } : configurationService.getValue<{ [key: string]: ITerminalProfileObject }>(isLinux ? TerminalSettingId.ProfilesLinux : TerminalSettingId.ProfilesMacOs), + typeof defaultProfile === 'string' ? defaultProfile : configurationService.getValue(isLinux ? TerminalSettingId.DefaultProfileLinux : TerminalSettingId.DefaultProfileMacOs), + testPwshSourcePaths, + variableResolver + ); +} + +async function detectAvailableWindowsProfiles( + includeDetectedProfiles: boolean, + fsProvider: IFsProvider, + logService?: ILogService, + useWslProfiles?: boolean, + configProfiles?: { [key: string]: ITerminalProfileObject }, + defaultProfileName?: string, + testPwshSourcePaths?: string[], + variableResolver?: (text: string[]) => Promise +): Promise { + // Determine the correct System32 path. We want to point to Sysnative + // when the 32-bit version of VS Code is running on a 64-bit machine. + // The reason for this is because PowerShell's important PSReadline + // module doesn't work if this is not the case. See #27915. + const is32ProcessOn64Windows = process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'); + const system32Path = `${process.env['windir']}\\${is32ProcessOn64Windows ? 'Sysnative' : 'System32'}`; + + let useWSLexe = false; + + if (getWindowsBuildNumber() >= 16299) { + useWSLexe = true; + } + + await initializeWindowsProfiles(testPwshSourcePaths); + + const detectedProfiles: Map = new Map(); + + // Add auto detected profiles + if (includeDetectedProfiles) { + detectedProfiles.set('PowerShell', { + source: ProfileSource.Pwsh, + icon: Codicon.terminalPowershell, + isAutoDetected: true + }); + detectedProfiles.set('Windows PowerShell', { + path: `${system32Path}\\WindowsPowerShell\\v1.0\\powershell.exe`, + icon: Codicon.terminalPowershell, + isAutoDetected: true + }); + detectedProfiles.set('Git Bash', { + source: ProfileSource.GitBash, + isAutoDetected: true + }); + detectedProfiles.set('Cygwin', { + path: [ + `${process.env['HOMEDRIVE']}\\cygwin64\\bin\\bash.exe`, + `${process.env['HOMEDRIVE']}\\cygwin\\bin\\bash.exe` + ], + args: ['--login'], + isAutoDetected: true + }); + detectedProfiles.set('Command Prompt', { + path: `${system32Path}\\cmd.exe`, + icon: Codicon.terminalCmd, + isAutoDetected: true + }); + } + + applyConfigProfilesToMap(configProfiles, detectedProfiles); + + const resultProfiles: ITerminalProfile[] = await transformToTerminalProfiles(detectedProfiles.entries(), defaultProfileName, fsProvider, logService, variableResolver); + + if (includeDetectedProfiles || (!includeDetectedProfiles && useWslProfiles)) { + try { + const result = await getWslProfiles(`${system32Path}\\${useWSLexe ? 'wsl' : 'bash'}.exe`, defaultProfileName); + for (const wslProfile of result) { + if (!configProfiles || !(wslProfile.profileName in configProfiles)) { + resultProfiles.push(wslProfile); + } + } + } catch (e) { + logService?.info('WSL is not installed, so could not detect WSL profiles'); + } + } + + return resultProfiles; +} + +async function transformToTerminalProfiles( + entries: IterableIterator<[string, ITerminalProfileObject]>, + defaultProfileName: string | undefined, + fsProvider: IFsProvider, + logService?: ILogService, + variableResolver?: (text: string[]) => Promise +): Promise { + const resultProfiles: ITerminalProfile[] = []; + for (const [profileName, profile] of entries) { + if (profile === null) { continue; } + let originalPaths: string[]; + let args: string[] | string | undefined; + let icon: ThemeIcon | URI | { light: URI, dark: URI } | undefined = undefined; + if ('source' in profile) { + const source = profileSources?.get(profile.source); + if (!source) { + continue; + } + originalPaths = source.paths; + + // if there are configured args, override the default ones + args = profile.args || source.args; + if (profile.icon) { + icon = profile.icon; + } else if (source.icon) { + icon = source.icon; + } + } else { + originalPaths = Array.isArray(profile.path) ? profile.path : [profile.path]; + args = isWindows ? profile.args : Array.isArray(profile.args) ? profile.args : undefined; + icon = profile.icon || undefined; + } + + const paths = (await variableResolver?.(originalPaths)) || originalPaths.slice(); + const validatedProfile = await validateProfilePaths(profileName, defaultProfileName, paths, fsProvider, args, profile.env, profile.overrideName, profile.isAutoDetected, logService); + if (validatedProfile) { + validatedProfile.isAutoDetected = profile.isAutoDetected; + validatedProfile.icon = icon; + validatedProfile.color = profile.color; + resultProfiles.push(validatedProfile); + } else { + logService?.trace('profile not validated', profileName, originalPaths); + } + } + return resultProfiles; +} + +async function initializeWindowsProfiles(testPwshSourcePaths?: string[]): Promise { + if (profileSources && !testPwshSourcePaths) { + return; + } + + profileSources = new Map(); + profileSources.set( + 'Git Bash', { + profileName: 'Git Bash', + paths: [ + `${process.env['ProgramW6432']}\\Git\\bin\\bash.exe`, + `${process.env['ProgramW6432']}\\Git\\usr\\bin\\bash.exe`, + `${process.env['ProgramFiles']}\\Git\\bin\\bash.exe`, + `${process.env['ProgramFiles']}\\Git\\usr\\bin\\bash.exe`, + `${process.env['LocalAppData']}\\Programs\\Git\\bin\\bash.exe`, + `${process.env['UserProfile']}\\scoop\\apps\\git-with-openssh\\current\\bin\\bash.exe`, + `${process.env['AllUsersProfile']}\\scoop\\apps\\git-with-openssh\\current\\bin\\bash.exe` + ], + args: ['--login'] + }); + profileSources.set('PowerShell', { + profileName: 'PowerShell', + paths: testPwshSourcePaths || await getPowershellPaths(), + icon: ThemeIcon.asThemeIcon(Codicon.terminalPowershell) + }); +} + +async function getPowershellPaths(): Promise { + const paths: string[] = []; + // Add all of the different kinds of PowerShells + for await (const pwshExe of enumeratePowerShellInstallations()) { + paths.push(pwshExe.exePath); + } + return paths; +} + +async function getWslProfiles(wslPath: string, defaultProfileName: string | undefined): Promise { + const profiles: ITerminalProfile[] = []; + const distroOutput = await new Promise((resolve, reject) => { + // wsl.exe output is encoded in utf16le (ie. A -> 0x4100) + cp.exec('wsl.exe -l -q', { encoding: 'utf16le' }, (err, stdout) => { + if (err) { + return reject('Problem occurred when getting wsl distros'); + } + resolve(stdout); + }); + }); + if (!distroOutput) { + return []; + } + const regex = new RegExp(/[\r?\n]/); + const distroNames = distroOutput.split(regex).filter(t => t.trim().length > 0 && t !== ''); + for (const distroName of distroNames) { + // Skip empty lines + if (distroName === '') { + continue; + } + + // docker-desktop and docker-desktop-data are treated as implementation details of + // Docker Desktop for Windows and therefore not exposed + if (distroName.startsWith('docker-desktop')) { + continue; + } + + // Create the profile, adding the icon depending on the distro + const profileName = `${distroName} (WSL)`; + const profile: ITerminalProfile = { + profileName, + path: wslPath, + args: [`-d`, `${distroName}`], + isDefault: profileName === defaultProfileName, + icon: getWslIcon(distroName) + }; + // Add the profile + profiles.push(profile); + } + return profiles; +} + +function getWslIcon(distroName: string): ThemeIcon { + if (distroName.includes('Ubuntu')) { + return ThemeIcon.asThemeIcon(Codicon.terminalUbuntu); + } else if (distroName.includes('Debian')) { + return ThemeIcon.asThemeIcon(Codicon.terminalDebian); + } else { + return ThemeIcon.asThemeIcon(Codicon.terminalLinux); + } +} + +async function detectAvailableUnixProfiles( + fsProvider: IFsProvider, + logService?: ILogService, + includeDetectedProfiles?: boolean, + configProfiles?: { [key: string]: ITerminalProfileObject }, + defaultProfileName?: string, + testPaths?: string[], + variableResolver?: (text: string[]) => Promise +): Promise { + const detectedProfiles: Map = new Map(); + + // Add non-quick launch profiles + if (includeDetectedProfiles) { + const contents = (await fsProvider.readFile('/etc/shells')).toString(); + const profiles = testPaths || contents.split('\n').filter(e => e.trim().indexOf('#') !== 0 && e.trim().length > 0); + const counts: Map = new Map(); + for (const profile of profiles) { + let profileName = basename(profile); + let count = counts.get(profileName) || 0; + count++; + if (count > 1) { + profileName = `${profileName} (${count})`; + } + counts.set(profileName, count); + detectedProfiles.set(profileName, { path: profile, isAutoDetected: true }); + } + } + + applyConfigProfilesToMap(configProfiles, detectedProfiles); + + return await transformToTerminalProfiles(detectedProfiles.entries(), defaultProfileName, fsProvider, logService, variableResolver); +} + +function applyConfigProfilesToMap(configProfiles: { [key: string]: ITerminalProfileObject } | undefined, profilesMap: Map) { + if (!configProfiles) { + return; + } + for (const [profileName, value] of Object.entries(configProfiles)) { + if (value === null || (!('path' in value) && !('source' in value))) { + profilesMap.delete(profileName); + } else { + profilesMap.set(profileName, value); + } + } +} + +async function validateProfilePaths(profileName: string, defaultProfileName: string | undefined, potentialPaths: string[], fsProvider: IFsProvider, args?: string[] | string, env?: ITerminalEnvironment, overrideName?: boolean, isAutoDetected?: boolean, logService?: ILogService): Promise { + if (potentialPaths.length === 0) { + return Promise.resolve(undefined); + } + const path = potentialPaths.shift()!; + if (path === '') { + return validateProfilePaths(profileName, defaultProfileName, potentialPaths, fsProvider, args, env, overrideName, isAutoDetected); + } + + const profile: ITerminalProfile = { profileName, path, args, env, overrideName, isAutoDetected, isDefault: profileName === defaultProfileName }; + + // For non-absolute paths, check if it's available on $PATH + if (basename(path) === path) { + // The executable isn't an absolute path, try find it on the PATH + const envPaths: string[] | undefined = process.env.PATH ? process.env.PATH.split(delimiter) : undefined; + const executable = await findExecutable(path, undefined, envPaths, undefined, fsProvider.existsFile); + if (!executable) { + return validateProfilePaths(profileName, defaultProfileName, potentialPaths, fsProvider, args); + } + return profile; + } + + const result = await fsProvider.existsFile(normalize(path)); + if (result) { + return profile; + } + + return validateProfilePaths(profileName, defaultProfileName, potentialPaths, fsProvider, args, env, overrideName, isAutoDetected); +} + +export interface IFsProvider { + existsFile(path: string): Promise, + readFile(path: string): Promise; +} + +export interface IProfileVariableResolver { + resolve(text: string[]): Promise; +} + +interface IPotentialTerminalProfile { + profileName: string; + paths: string[]; + args?: string[]; + icon?: ThemeIcon | URI | { light: URI, dark: URI }; +} diff --git a/src/vs/platform/terminal/node/windowsShellHelper.ts b/src/vs/platform/terminal/node/windowsShellHelper.ts index 207bfaff20..6de821ec4a 100644 --- a/src/vs/platform/terminal/node/windowsShellHelper.ts +++ b/src/vs/platform/terminal/node/windowsShellHelper.ts @@ -38,15 +38,15 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe private _isDisposed: boolean; private _currentRequest: Promise | undefined; private _shellType: TerminalShellType | undefined; - public get shellType(): TerminalShellType | undefined { return this._shellType; } + get shellType(): TerminalShellType | undefined { return this._shellType; } private _shellTitle: string = ''; - public get shellTitle(): string { return this._shellTitle; } + get shellTitle(): string { return this._shellTitle; } private readonly _onShellNameChanged = new Emitter(); - public get onShellNameChanged(): Event { return this._onShellNameChanged.event; } + get onShellNameChanged(): Event { return this._onShellNameChanged.event; } private readonly _onShellTypeChanged = new Emitter(); - public get onShellTypeChanged(): Event { return this._onShellTypeChanged.event; } + get onShellTypeChanged(): Event { return this._onShellTypeChanged.event; } - public constructor( + constructor( private _rootProcessId: number ) { super(); @@ -112,7 +112,7 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe return this.traverseTree(tree.children[favouriteChild]); } - public override dispose(): void { + override dispose(): void { this._isDisposed = true; super.dispose(); } @@ -120,7 +120,7 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe /** * Returns the innermost shell executable running in the terminal */ - public getShellName(): Promise { + getShellName(): Promise { if (this._isDisposed) { return Promise.resolve(''); } @@ -141,7 +141,7 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe return this._currentRequest; } - public getShellType(executable: string): TerminalShellType { + getShellType(executable: string): TerminalShellType { switch (executable.toLowerCase()) { case 'cmd.exe': return WindowsShellType.CommandPrompt; diff --git a/src/vs/platform/terminal/test/common/terminalRecorder.test.ts b/src/vs/platform/terminal/test/common/terminalRecorder.test.ts index 007ce51448..ea16e9a7aa 100644 --- a/src/vs/platform/terminal/test/common/terminalRecorder.test.ts +++ b/src/vs/platform/terminal/test/common/terminalRecorder.test.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index ecd6ceac5a..bdd1a1125f 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -11,6 +11,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import * as nls from 'vs/nls'; import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { RunOnceScheduler } from 'vs/base/common/async'; +import { assertNever } from 'vs/base/common/types'; // ------ API types @@ -24,11 +25,21 @@ export interface ColorContribution { readonly deprecationMessage: string | undefined; } - -export interface ColorFunction { - (theme: IColorTheme): Color | undefined; +export const enum ColorTransformType { + Darken, + Lighten, + Transparent, + OneOf, + LessProminent, } +export type ColorTransform = + | { op: ColorTransformType.Darken; value: ColorValue; factor: number } + | { op: ColorTransformType.Lighten; value: ColorValue; factor: number } + | { op: ColorTransformType.Transparent; value: ColorValue; factor: number } + | { op: ColorTransformType.OneOf; values: readonly ColorValue[] } + | { op: ColorTransformType.LessProminent; value: ColorValue; background: ColorValue; factor: number; transparency: number }; + export interface ColorDefaults { light: ColorValue | null; dark: ColorValue | null; @@ -38,7 +49,7 @@ export interface ColorDefaults { /** * A Color Value is either a color literal, a reference to an other color or a derived color */ -export type ColorValue = Color | string | ColorIdentifier | ColorFunction; +export type ColorValue = Color | string | ColorIdentifier | ColorTransform; // color registry export const Extensions = { @@ -356,8 +367,8 @@ export const editorActiveLinkForeground = registerColor('editorLink.activeForegr /** * 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')); +export const editorInlayHintForeground = registerColor('editorInlayHint.foreground', { dark: editorWidgetBackground, light: editorWidgetForeground, hc: editorWidgetBackground }, nls.localize('editorInlayHintForeground', 'Foreground color of inline hints')); +export const editorInlayHintBackground = registerColor('editorInlayHint.background', { dark: editorWidgetForeground, light: editorWidgetBackground, hc: editorWidgetForeground }, nls.localize('editorInlayHintBackground', 'Background color of inline hints')); /** * Editor lighbulb icon colors @@ -395,7 +406,8 @@ export const listInactiveFocusOutline = registerColor('list.inactiveFocusOutline export const listHoverBackground = registerColor('list.hoverBackground', { dark: '#2A2D2E', light: '#F0F0F0', hc: null }, nls.localize('listHoverBackground', "List/Tree background when hovering over items using the mouse.")); export const listHoverForeground = registerColor('list.hoverForeground', { dark: null, light: null, hc: null }, nls.localize('listHoverForeground', "List/Tree foreground when hovering over items using the mouse.")); export const listDropBackground = registerColor('list.dropBackground', { dark: '#062F4A', light: '#D6EBFF', hc: null }, nls.localize('listDropBackground', "List/Tree drag and drop background when moving items around using the mouse.")); -export const listHighlightForeground = registerColor('list.highlightForeground', { dark: '#0097fb', light: '#0066BF', hc: focusBorder }, nls.localize('highlight', 'List/Tree foreground color of the match highlights when searching inside the list/tree.')); +export const listHighlightForeground = registerColor('list.highlightForeground', { dark: '#18A3FF', light: '#0066BF', hc: focusBorder }, nls.localize('highlight', 'List/Tree foreground color of the match highlights when searching inside the list/tree.')); +export const listFocusHighlightForeground = registerColor('list.focusHighlightForeground', { dark: listHighlightForeground, light: listHighlightForeground, hc: listHighlightForeground }, nls.localize('listFocusHighlightForeground', 'List/Tree foreground color of the match highlights on actively focused items when searching inside the list/tree.')); export const listInvalidItemForeground = registerColor('list.invalidItemForeground', { dark: '#B89500', light: '#B89500', hc: '#B89500' }, nls.localize('invalidItemForeground', 'List/Tree foreground color for invalid items, for example an unresolved root in explorer.')); export const listErrorForeground = registerColor('list.errorForeground', { dark: '#F88070', light: '#B01011', hc: null }, nls.localize('listErrorForeground', 'Foreground color of list items containing errors.')); export const listWarningForeground = registerColor('list.warningForeground', { dark: '#CCA700', light: '#855F00', hc: null }, nls.localize('listWarningForeground', 'Foreground color of list items containing warnings.')); @@ -412,7 +424,8 @@ export const listDeemphasizedForeground = registerColor('list.deemphasizedForegr * Quick pick widget (dependent on List and tree colors) */ export const _deprecatedQuickInputListFocusBackground = registerColor('quickInput.list.focusBackground', { dark: null, light: null, hc: null }, '', undefined, nls.localize('quickInput.list.focusBackground deprecation', "Please use quickInputList.focusBackground instead")); -export const quickInputListFocusBackground = registerColor('quickInputList.focusBackground', { dark: oneOf(_deprecatedQuickInputListFocusBackground, listFocusBackground, '#062F4A'), light: oneOf(_deprecatedQuickInputListFocusBackground, listFocusBackground, '#D6EBFF'), hc: null }, nls.localize('quickInput.listFocusBackground', "Quick picker background color for the focused item.")); +export const quickInputListFocusForeground = registerColor('quickInputList.focusForeground', { dark: listActiveSelectionForeground, light: listActiveSelectionForeground, hc: listActiveSelectionForeground }, nls.localize('quickInput.listFocusForeground', "Quick picker foreground color for the focused item.")); +export const quickInputListFocusBackground = registerColor('quickInputList.focusBackground', { dark: oneOf(_deprecatedQuickInputListFocusBackground, listActiveSelectionBackground, '#062F4A'), light: oneOf(_deprecatedQuickInputListFocusBackground, listActiveSelectionBackground, '#D6EBFF'), hc: null }, nls.localize('quickInput.listFocusBackground', "Quick picker background color for the focused item.")); /** * Menu colors @@ -505,63 +518,63 @@ export const chartsPurple = registerColor('charts.purple', { dark: '#B180D7', li // ----- color functions -export function darken(colorValue: ColorValue, factor: number): ColorFunction { - return (theme) => { - let color = resolveColorValue(colorValue, theme); - if (color) { - return color.darken(factor); - } - return undefined; - }; -} +export function executeTransform(transform: ColorTransform, theme: IColorTheme) { + switch (transform.op) { + case ColorTransformType.Darken: + return resolveColorValue(transform.value, theme)?.darken(transform.factor); -export function lighten(colorValue: ColorValue, factor: number): ColorFunction { - return (theme) => { - let color = resolveColorValue(colorValue, theme); - if (color) { - return color.lighten(factor); - } - return undefined; - }; -} + case ColorTransformType.Lighten: + return resolveColorValue(transform.value, theme)?.lighten(transform.factor); -export function transparent(colorValue: ColorValue, factor: number): ColorFunction { - return (theme) => { - let color = resolveColorValue(colorValue, theme); - if (color) { - return color.transparent(factor); - } - return undefined; - }; -} + case ColorTransformType.Transparent: + return resolveColorValue(transform.value, theme)?.transparent(transform.factor); -export function oneOf(...colorValues: ColorValue[]): ColorFunction { - return (theme) => { - for (let colorValue of colorValues) { - let color = resolveColorValue(colorValue, theme); - if (color) { - return color; - } - } - return undefined; - }; -} - -function lessProminent(colorValue: ColorValue, backgroundColorValue: ColorValue, factor: number, transparency: number): ColorFunction { - return (theme) => { - let from = resolveColorValue(colorValue, theme); - if (from) { - let backgroundColor = resolveColorValue(backgroundColorValue, theme); - if (backgroundColor) { - if (from.isDarkerThan(backgroundColor)) { - return Color.getLighterColor(from, backgroundColor, factor).transparent(transparency); + case ColorTransformType.OneOf: + for (const candidate of transform.values) { + const color = resolveColorValue(candidate, theme); + if (color) { + return color; } - return Color.getDarkerColor(from, backgroundColor, factor).transparent(transparency); } - return from.transparent(factor * transparency); - } - return undefined; - }; + return undefined; + + case ColorTransformType.LessProminent: + const from = resolveColorValue(transform.value, theme); + if (!from) { + return undefined; + } + + const backgroundColor = resolveColorValue(transform.background, theme); + if (!backgroundColor) { + return from.transparent(transform.factor * transform.transparency); + } + + return from.isDarkerThan(backgroundColor) + ? Color.getLighterColor(from, backgroundColor, transform.factor).transparent(transform.transparency) + : Color.getDarkerColor(from, backgroundColor, transform.factor).transparent(transform.transparency); + default: + throw assertNever(transform); + } +} + +export function darken(colorValue: ColorValue, factor: number): ColorTransform { + return { op: ColorTransformType.Darken, value: colorValue, factor }; +} + +export function lighten(colorValue: ColorValue, factor: number): ColorTransform { + return { op: ColorTransformType.Lighten, value: colorValue, factor }; +} + +export function transparent(colorValue: ColorValue, factor: number): ColorTransform { + return { op: ColorTransformType.Transparent, value: colorValue, factor }; +} + +export function oneOf(...colorValues: ColorValue[]): ColorTransform { + return { op: ColorTransformType.OneOf, values: colorValues }; +} + +function lessProminent(colorValue: ColorValue, backgroundColorValue: ColorValue, factor: number, transparency: number): ColorTransform { + return { op: ColorTransformType.LessProminent, value: colorValue, background: backgroundColorValue, factor, transparency }; } // ----- implementation @@ -579,8 +592,8 @@ export function resolveColorValue(colorValue: ColorValue | null, theme: IColorTh return theme.getColor(colorValue); } else if (colorValue instanceof Color) { return colorValue; - } else if (typeof colorValue === 'function') { - return colorValue(theme); + } else if (typeof colorValue === 'object') { + return executeTransform(colorValue, theme); } return undefined; } diff --git a/src/vs/platform/theme/common/styler.ts b/src/vs/platform/theme/common/styler.ts index 256e5bab17..63c9dbbc38 100644 --- a/src/vs/platform/theme/common/styler.ts +++ b/src/vs/platform/theme/common/styler.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, contrastBorder, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, badgeBackground, badgeForeground, progressBarBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, breadcrumbsBackground, editorWidgetBorder, inputValidationInfoForeground, inputValidationWarningForeground, inputValidationErrorForeground, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuBorder, menuSeparatorBackground, listFilterWidgetOutline, listFilterWidgetNoMatchesOutline, listFilterWidgetBackground, editorWidgetBackground, treeIndentGuidesStroke, editorWidgetForeground, simpleCheckboxBackground, simpleCheckboxBorder, simpleCheckboxForeground, ColorValue, resolveColorValue, textLinkForeground, problemsWarningIconForeground, problemsErrorIconForeground, problemsInfoIconForeground, buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground, listFocusOutline, listInactiveFocusOutline, tableColumnsBorder, quickInputListFocusBackground, buttonBorder, keybindingLabelForeground, keybindingLabelBackground, keybindingLabelBorder, keybindingLabelBottomBorder, buttonDisabledBackground, buttonDisabledBorder, buttonDisabledForeground, buttonSecondaryBorder } from 'vs/platform/theme/common/colorRegistry'; // {{SQL CARBON EDIT}} Add button colors +import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, contrastBorder, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, badgeBackground, badgeForeground, progressBarBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, breadcrumbsBackground, editorWidgetBorder, inputValidationInfoForeground, inputValidationWarningForeground, inputValidationErrorForeground, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuBorder, menuSeparatorBackground, listFilterWidgetOutline, listFilterWidgetNoMatchesOutline, listFilterWidgetBackground, editorWidgetBackground, treeIndentGuidesStroke, editorWidgetForeground, simpleCheckboxBackground, simpleCheckboxBorder, simpleCheckboxForeground, ColorValue, resolveColorValue, textLinkForeground, problemsWarningIconForeground, problemsErrorIconForeground, problemsInfoIconForeground, buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground, listFocusOutline, listInactiveFocusOutline, tableColumnsBorder, quickInputListFocusBackground, buttonBorder, keybindingLabelForeground, keybindingLabelBackground, keybindingLabelBorder, keybindingLabelBottomBorder, quickInputListFocusForeground, ColorTransform, buttonDisabledBackground, buttonDisabledBorder, buttonDisabledForeground, buttonSecondaryBorder } from 'vs/platform/theme/common/colorRegistry'; // {{SQL CARBON EDIT}} Add button colors import { IDisposable } from 'vs/base/common/lifecycle'; import { Color } from 'vs/base/common/color'; import { IThemable, styleFn } from 'vs/base/common/styler'; @@ -35,7 +35,7 @@ export function computeStyles(theme: IColorTheme, styleMap: IColorMapping): ICom } export function attachStyler(themeService: IThemeService, styleMap: T, widgetOrCallback: IThemable | styleFn): IDisposable { - function applyStyles(theme: IColorTheme): void { + function applyStyles(): void { const styles = computeStyles(themeService.getColorTheme(), styleMap); if (typeof widgetOrCallback === 'function') { @@ -45,7 +45,7 @@ export function attachStyler(themeService: IThemeServic } } - applyStyles(themeService.getColorTheme()); + applyStyles(); return themeService.onDidColorThemeChange(applyStyles); } @@ -130,7 +130,7 @@ export function attachSelectBoxStyler(widget: IThemable, themeService: IThemeSer selectBorder: style?.selectBorder || selectBorder, focusBorder: style?.focusBorder || focusBorder, listFocusBackground: style?.listFocusBackground || quickInputListFocusBackground, - listFocusForeground: style?.listFocusForeground || listFocusForeground, + listFocusForeground: style?.listFocusForeground || quickInputListFocusForeground, listFocusOutline: style?.listFocusOutline || ((theme: IColorTheme) => theme.type === ColorScheme.HIGH_CONTRAST ? activeContrastBorder : Color.transparent), listHoverBackground: style?.listHoverBackground || listHoverBackground, listHoverForeground: style?.listHoverForeground || listHoverForeground, @@ -264,16 +264,6 @@ export function attachKeybindingLabelStyler(widget: IThemable, themeService: ITh } as IKeybindingLabelStyleOverrides, widget); } -export interface ILinkStyleOverrides extends IStyleOverrides { - textLinkForeground?: ColorIdentifier; -} - -export function attachLinkStyler(widget: IThemable, themeService: IThemeService, style?: ILinkStyleOverrides): IDisposable { - return attachStyler(themeService, { - textLinkForeground: style?.textLinkForeground || textLinkForeground, - } as ILinkStyleOverrides, widget); -} - export interface IProgressBarStyleOverrides extends IStyleOverrides { progressBarBackground?: ColorIdentifier; } @@ -289,7 +279,7 @@ export function attachStylerCallback(themeService: IThemeService, colors: { [nam } export interface IBreadcrumbsWidgetStyleOverrides extends IColorMapping { - breadcrumbsBackground?: ColorIdentifier | ColorFunction; + breadcrumbsBackground?: ColorIdentifier | ColorTransform; breadcrumbsForeground?: ColorIdentifier; breadcrumbsHoverForeground?: ColorIdentifier; breadcrumbsFocusForeground?: ColorIdentifier; @@ -334,7 +324,7 @@ export function attachMenuStyler(widget: IThemable, themeService: IThemeService, return attachStyler(themeService, { ...defaultMenuStyles, ...style }, widget); } -export interface IDialogStyleOverrides extends IButtonStyleOverrides, ILinkStyleOverrides { +export interface IDialogStyleOverrides extends IButtonStyleOverrides { dialogForeground?: ColorIdentifier; dialogBackground?: ColorIdentifier; dialogShadow?: ColorIdentifier; diff --git a/src/vs/platform/theme/common/themeService.ts b/src/vs/platform/theme/common/themeService.ts index dad895b559..3e6bd567ff 100644 --- a/src/vs/platform/theme/common/themeService.ts +++ b/src/vs/platform/theme/common/themeService.ts @@ -67,6 +67,10 @@ export namespace ThemeIcon { return ti1.id === ti2.id && ti1.color?.id === ti2.color?.id; } + export function asThemeIcon(codicon: Codicon): ThemeIcon { + return { id: codicon.id }; + } + export const asClassNameArray: (icon: ThemeIcon) => string[] = CSSIcon.asClassNameArray; export const asClassName: (icon: ThemeIcon) => string = CSSIcon.asClassName; export const asCSSSelector: (icon: ThemeIcon) => string = CSSIcon.asCSSSelector; diff --git a/src/vs/platform/theme/electron-main/themeMainService.ts b/src/vs/platform/theme/electron-main/themeMainService.ts index abdfc6d3b6..40ea1080fa 100644 --- a/src/vs/platform/theme/electron-main/themeMainService.ts +++ b/src/vs/platform/theme/electron-main/themeMainService.ts @@ -3,10 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { BrowserWindow, nativeTheme } from 'electron'; import { isWindows, isMacintosh } from 'vs/base/common/platform'; -import { ipcMain, nativeTheme } from 'electron'; -import { IStateService } from 'vs/platform/state/node/state'; +import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IPartsSplash } from 'vs/platform/windows/common/windows'; const DEFAULT_BG_LIGHT = '#FFFFFF'; const DEFAULT_BG_DARK = '#1E1E1E'; @@ -14,45 +15,38 @@ const DEFAULT_BG_HC_BLACK = '#000000'; const THEME_STORAGE_KEY = 'theme'; const THEME_BG_STORAGE_KEY = 'themeBackground'; +const THEME_WINDOW_SPLASH = 'windowSplash'; export const IThemeMainService = createDecorator('themeMainService'); export interface IThemeMainService { + readonly _serviceBrand: undefined; getBackgroundColor(): string; + + saveWindowSplash(windowId: number | undefined, splash: IPartsSplash): void; + getWindowSplash(): IPartsSplash | undefined; } export class ThemeMainService implements IThemeMainService { declare readonly _serviceBrand: undefined; - constructor(@IStateService private stateService: IStateService) { - ipcMain.on('vscode:changeColorTheme', (e: Event, windowId: number, broadcast: string) => { - // Theme changes - if (typeof broadcast === 'string') { - this.storeBackgroundColor(JSON.parse(broadcast)); - } - }); - } - - private storeBackgroundColor(data: { baseTheme: string, background: string }): void { - this.stateService.setItem(THEME_STORAGE_KEY, data.baseTheme); - this.stateService.setItem(THEME_BG_STORAGE_KEY, data.background); - } + constructor(@IStateMainService private stateMainService: IStateMainService) { } getBackgroundColor(): string { if ((isWindows || isMacintosh) && nativeTheme.shouldUseInvertedColorScheme) { return DEFAULT_BG_HC_BLACK; } - let background = this.stateService.getItem(THEME_BG_STORAGE_KEY, null); + let background = this.stateMainService.getItem(THEME_BG_STORAGE_KEY, null); if (!background) { let baseTheme: string; if ((isWindows || isMacintosh) && nativeTheme.shouldUseInvertedColorScheme) { baseTheme = 'hc-black'; } else { - baseTheme = this.stateService.getItem(THEME_STORAGE_KEY, 'vs-dark').split(' ')[0]; + baseTheme = this.stateMainService.getItem(THEME_STORAGE_KEY, 'vs-dark').split(' ')[0]; } background = (baseTheme === 'hc-black') ? DEFAULT_BG_HC_BLACK : (baseTheme === 'vs' ? DEFAULT_BG_LIGHT : DEFAULT_BG_DARK); @@ -64,4 +58,32 @@ export class ThemeMainService implements IThemeMainService { return background; } + + saveWindowSplash(windowId: number | undefined, splash: IPartsSplash): void { + + // Update in storage + this.stateMainService.setItems([ + { key: THEME_STORAGE_KEY, data: splash.baseTheme }, + { key: THEME_BG_STORAGE_KEY, data: splash.colorInfo.background }, + { key: THEME_WINDOW_SPLASH, data: splash } + ]); + + // Update in opened windows + if (typeof windowId === 'number') { + this.updateBackgroundColor(windowId, splash); + } + } + + private updateBackgroundColor(windowId: number, splash: IPartsSplash): void { + for (const window of BrowserWindow.getAllWindows()) { + if (window.id === windowId) { + window.setBackgroundColor(splash.colorInfo.background); + break; + } + } + } + + getWindowSplash(): IPartsSplash | undefined { + return this.stateMainService.getItem(THEME_WINDOW_SPLASH); + } } diff --git a/src/vs/platform/undoRedo/common/undoRedoService.ts b/src/vs/platform/undoRedo/common/undoRedoService.ts index 31edd8887c..cbfd7d2d11 100644 --- a/src/vs/platform/undoRedo/common/undoRedoService.ts +++ b/src/vs/platform/undoRedo/common/undoRedoService.ts @@ -709,7 +709,7 @@ export class UndoRedoService implements IUndoRedoService { private _onError(err: Error, element: StackElement): void { onUnexpectedError(err); - // An error occured while undoing or redoing => drop the undo/redo stack for all affected resources + // An error occurred while undoing or redoing => drop the undo/redo stack for all affected resources for (const strResource of element.strResources) { this.removeElements(strResource); } diff --git a/src/vs/platform/update/common/updateIpc.ts b/src/vs/platform/update/common/updateIpc.ts index 6340754edb..a572fb430c 100644 --- a/src/vs/platform/update/common/updateIpc.ts +++ b/src/vs/platform/update/common/updateIpc.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * 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'; diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index e4a361a955..bea3370d5f 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -164,7 +164,7 @@ export abstract class AbstractUpdateService implements IUpdateService { this.logService.trace('update#quitAndInstall(): before lifecycle quit()'); - this.lifecycleMainService.quit(true /* from update */).then(vetod => { + this.lifecycleMainService.quit(true /* will restart */).then(vetod => { this.logService.trace(`update#quitAndInstall(): after lifecycle quit() with veto: ${vetod}`); if (vetod) { return; diff --git a/src/vs/platform/update/electron-main/updateService.snap.ts b/src/vs/platform/update/electron-main/updateService.snap.ts index 19eaf3e606..68007c296c 100644 --- a/src/vs/platform/update/electron-main/updateService.snap.ts +++ b/src/vs/platform/update/electron-main/updateService.snap.ts @@ -106,7 +106,7 @@ abstract class AbstractUpdateService implements IUpdateService { this.logService.trace('update#quitAndInstall(): before lifecycle quit()'); - this.lifecycleMainService.quit(true /* from update */).then(vetod => { + this.lifecycleMainService.quit(true /* will restart */).then(vetod => { this.logService.trace(`update#quitAndInstall(): after lifecycle quit() with veto: ${vetod}`); if (vetod) { return; diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 756ae0a6cf..190c96f47a 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -53,8 +53,8 @@ export class Win32UpdateService extends AbstractUpdateService { @memoize get cachePath(): Promise { - const result = path.join(tmpdir(), `sqlops-update-${this.productService.target}-${process.arch}`); - return fs.promises.mkdir(result, { recursive: true }).then(() => result); + const result = path.join(tmpdir(), `sqlops-update-${this.productService.target}-${process.arch}`); // {{SQL CARBON EDIT}} + return pfs.Promises.mkdir(result, { recursive: true }).then(() => result); } constructor( @@ -133,7 +133,7 @@ export class Win32UpdateService extends AbstractUpdateService { return this.cleanup(update.version).then(() => { return this.getUpdatePackagePath(update.version).then(updatePackagePath => { - return pfs.exists(updatePackagePath).then(exists => { + return pfs.Promises.exists(updatePackagePath).then(exists => { if (exists) { return Promise.resolve(updatePackagePath); } @@ -145,7 +145,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(() => fs.promises.rename(downloadPath, updatePackagePath)) + .then(() => pfs.Promises.rename(downloadPath, updatePackagePath)) .then(() => updatePackagePath); }); }).then(packagePath => { @@ -191,11 +191,11 @@ export class Win32UpdateService extends AbstractUpdateService { const filter = exceptVersion ? (one: string) => !(new RegExp(`${this.productService.quality}-${exceptVersion}\\.exe$`).test(one)) : () => true; const cachePath = await this.cachePath; - const versions = await pfs.readdir(cachePath); + const versions = await pfs.Promises.readdir(cachePath); const promises = versions.filter(filter).map(async one => { try { - await fs.promises.unlink(path.join(cachePath, one)); + await pfs.Promises.unlink(path.join(cachePath, one)); } catch (err) { // ignore } @@ -220,7 +220,7 @@ export class Win32UpdateService extends AbstractUpdateService { this.availableUpdate.updateFilePath = path.join(cachePath, `CodeSetup-${this.productService.quality}-${update.version}.flag`); - await pfs.writeFile(this.availableUpdate.updateFilePath, 'flag'); + await pfs.Promises.writeFile(this.availableUpdate.updateFilePath, 'flag'); const child = spawn(this.availableUpdate.packagePath, ['/verysilent', `/update="${this.availableUpdate.updateFilePath}"`, '/nocloseapplications', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], { detached: true, stdio: ['ignore', 'ignore', 'ignore'], diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index a9d03b5ca4..737a84a28a 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -10,7 +10,7 @@ import { URI } from 'vs/base/common/uri'; import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncResourcePreview as IBaseSyncResourcePreview, - IUserDataManifest, ISyncData, IRemoteUserData, PREVIEW_DIR_NAME, IResourcePreview as IBaseResourcePreview, Change, MergeState, IUserDataInitializer + IUserDataManifest, ISyncData, IRemoteUserData, PREVIEW_DIR_NAME, IResourcePreview as IBaseResourcePreview, Change, MergeState, IUserDataInitializer, getLastSyncResourceUri } from 'vs/platform/userDataSync/common/userDataSync'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IExtUri, extUri, extUriIgnorePathCase } from 'vs/base/common/resources'; @@ -53,10 +53,6 @@ export function isSyncData(thing: any): thing is ISyncData { return false; } -function getLastSyncResourceUri(syncResource: SyncResource, environmentService: IEnvironmentService, extUri: IExtUri): URI { - return extUri.joinPath(environmentService.userDataSyncHome, syncResource, `lastSync${syncResource}.json`); -} - export interface IResourcePreview { readonly remoteResource: URI; @@ -273,9 +269,10 @@ export abstract class AbstractSynchroniser extends Disposable { this.setStatus(SyncStatus.Syncing); const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getLatestRemoteUserData(null, lastSyncUserData); + const isRemoteDataFromCurrentMachine = await this.isRemoteDataFromCurrentMachine(remoteUserData); /* use replace sync data */ - const resourcePreviewResults = await this.generateSyncPreview({ ref: remoteUserData.ref, syncData }, lastSyncUserData, CancellationToken.None); + const resourcePreviewResults = await this.generateSyncPreview({ ref: remoteUserData.ref, syncData }, lastSyncUserData, isRemoteDataFromCurrentMachine, CancellationToken.None); const resourcePreviews: [IResourcePreview, IAcceptResult][] = []; for (const resourcePreviewResult of resourcePreviewResults) { @@ -295,6 +292,11 @@ export abstract class AbstractSynchroniser extends Disposable { return true; } + private async isRemoteDataFromCurrentMachine(remoteUserData: IRemoteUserData): Promise { + const machineId = await this.currentMachineIdPromise; + return !!remoteUserData.syncData?.machineId && remoteUserData.syncData.machineId === machineId; + } + protected async getLatestRemoteUserData(manifest: IUserDataManifest | null, lastSyncUserData: IRemoteUserData | null): Promise { if (lastSyncUserData) { @@ -564,11 +566,8 @@ export abstract class AbstractSynchroniser extends Disposable { } private async doGenerateSyncResourcePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean, token: CancellationToken): Promise { - const machineId = await this.currentMachineIdPromise; - const isLastSyncFromCurrentMachine = !!remoteUserData.syncData?.machineId && remoteUserData.syncData.machineId === machineId; - - const lastSyncUserDataForPreview = lastSyncUserData === null && isLastSyncFromCurrentMachine && !this.hasSyncResourceStateVersionChanged ? remoteUserData : lastSyncUserData; - const resourcePreviewResults = await this.generateSyncPreview(remoteUserData, lastSyncUserDataForPreview, token); + const isRemoteDataFromCurrentMachine = await this.isRemoteDataFromCurrentMachine(remoteUserData); + const resourcePreviewResults = await this.generateSyncPreview(remoteUserData, lastSyncUserData, isRemoteDataFromCurrentMachine, token); const resourcePreviews: IEditableResourcePreview[] = []; for (const resourcePreviewResult of resourcePreviewResults) { @@ -609,7 +608,7 @@ export abstract class AbstractSynchroniser extends Disposable { } } - return { remoteUserData, lastSyncUserData, resourcePreviews, isLastSyncFromCurrentMachine }; + return { remoteUserData, lastSyncUserData, resourcePreviews, isLastSyncFromCurrentMachine: isRemoteDataFromCurrentMachine }; } async getLastSyncUserData(): Promise { @@ -716,7 +715,7 @@ export abstract class AbstractSynchroniser extends Disposable { } protected abstract readonly version: number; - protected abstract generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise; + protected abstract generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, isRemoteDataFromCurrentMachine: boolean, token: CancellationToken): Promise; protected abstract getMergeResult(resourcePreview: IResourcePreview, token: CancellationToken): Promise; protected abstract getAcceptResult(resourcePreview: IResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise; protected abstract applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, result: [IResourcePreview, IAcceptResult][], force: boolean): Promise; diff --git a/src/vs/platform/userDataSync/common/extensionsMerge.ts b/src/vs/platform/userDataSync/common/extensionsMerge.ts index cc9381fc53..898eaf5f3c 100644 --- a/src/vs/platform/userDataSync/common/extensionsMerge.ts +++ b/src/vs/platform/userDataSync/common/extensionsMerge.ts @@ -10,10 +10,8 @@ import { IStringDictionary } from 'vs/base/common/collections'; import * as semver from 'vs/base/common/semver/semver'; export interface IMergeResult { - added: ISyncExtension[]; - removed: IExtensionIdentifier[]; - updated: ISyncExtensionWithVersion[]; - remote: ISyncExtension[] | null; + readonly local: { added: ISyncExtension[], removed: IExtensionIdentifier[], updated: ISyncExtension[] }; + readonly remote: { added: ISyncExtension[], removed: ISyncExtension[], updated: ISyncExtension[], all: ISyncExtension[] } | null; } export function merge(localExtensions: ISyncExtensionWithVersion[], remoteExtensions: ISyncExtension[] | null, lastSyncExtensions: ISyncExtension[] | null, skippedExtensions: ISyncExtension[], ignoredExtensions: string[]): IMergeResult { @@ -24,10 +22,17 @@ export function merge(localExtensions: ISyncExtensionWithVersion[], remoteExtens if (!remoteExtensions) { const remote = localExtensions.filter(({ identifier }) => ignoredExtensions.every(id => id.toLowerCase() !== identifier.id.toLowerCase())); return { - added, - removed, - updated, - remote: remote.length > 0 ? remote : null + local: { + added, + removed, + updated, + }, + remote: remote.length > 0 ? { + added: remote, + updated: [], + removed: [], + all: remote + } : null }; } @@ -179,7 +184,15 @@ export function merge(localExtensions: ISyncExtensionWithVersion[], remoteExtens newRemoteExtensionsMap.forEach((value, key) => remote.push(massageOutgoingExtension(value, key))); } - return { added, removed, updated, remote: remote.length ? remote : null }; + return { + local: { added, removed, updated }, + remote: remote.length ? { + added: [...remoteChanges.added].map(id => newRemoteExtensionsMap.get(id)!), + updated: [...remoteChanges.updated].map(id => newRemoteExtensionsMap.get(id)!), + removed: [...remoteChanges.removed].map(id => remoteExtensionsMap.get(id)!), + all: remote + } : null + }; } function compare(from: Map | null, to: Map, ignoredExtensions: Set, { checkInstalledProperty, checkVersionProperty }: { checkInstalledProperty: boolean, checkVersionProperty: boolean } = { checkInstalledProperty: false, checkVersionProperty: false }): { added: Set, removed: Set, updated: Set } { diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index 9a5ff32c00..df00550602 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -14,7 +14,7 @@ import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/comm 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'; +import { merge, IMergeResult as IExtensionMergeResult } from 'vs/platform/userDataSync/common/extensionsMerge'; import { AbstractInitializer, AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; @@ -29,12 +29,7 @@ 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[]; - readonly removed: IExtensionIdentifier[]; - readonly updated: ISyncExtension[]; - readonly remote: ISyncExtension[] | null; -} +type IExtensionResourceMergeResult = IAcceptResult & IExtensionMergeResult; interface IExtensionResourcePreview extends IResourcePreview { readonly localExtensions: ISyncExtensionWithVersion[]; @@ -143,14 +138,11 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse this.logService.trace(`${this.syncResourceLogLabel}: Remote extensions does not exist. Synchronizing extensions for the first time.`); } - const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions); + const { local, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions); const previewResult: IExtensionResourceMergeResult = { - added, - removed, - updated, - remote, - content: this.getPreviewContent(localExtensions, added, updated, removed), - localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None, + local, remote, + content: this.getPreviewContent(localExtensions, local.added, local.updated, local.removed), + localChange: local.added.length > 0 || local.removed.length > 0 || local.updated.length > 0 ? Change.Modified : Change.None, remoteChange: remote !== null ? Change.Modified : Change.None, }; @@ -221,14 +213,12 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const installedExtensions = await this.extensionManagementService.getInstalled(); const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions); const mergeResult = merge(resourcePreview.localExtensions, null, null, resourcePreview.skippedExtensions, ignoredExtensions); - const { added, removed, updated, remote } = mergeResult; + const { local, remote } = mergeResult; return { content: resourcePreview.localContent, - added, - removed, - updated, + local, remote, - localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None, + localChange: local.added.length > 0 || local.removed.length > 0 || local.updated.length > 0 ? Change.Modified : Change.None, remoteChange: remote !== null ? Change.Modified : Change.None, }; } @@ -239,20 +229,19 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const remoteExtensions = resourcePreview.remoteContent ? JSON.parse(resourcePreview.remoteContent) : null; if (remoteExtensions !== null) { const mergeResult = merge(resourcePreview.localExtensions, remoteExtensions, resourcePreview.localExtensions, [], ignoredExtensions); - const { added, removed, updated, remote } = mergeResult; + const { local, remote } = mergeResult; return { content: resourcePreview.remoteContent, - added, - removed, - updated, + local, remote, - localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None, + localChange: local.added.length > 0 || local.removed.length > 0 || local.updated.length > 0 ? Change.Modified : Change.None, remoteChange: remote !== null ? Change.Modified : Change.None, }; } else { return { content: resourcePreview.remoteContent, - added: [], removed: [], updated: [], remote: null, + local: { added: [], removed: [], updated: [] }, + remote: null, localChange: Change.None, remoteChange: Change.None, }; @@ -261,7 +250,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [IExtensionResourcePreview, IExtensionResourceMergeResult][], force: boolean): Promise { let { skippedExtensions, localExtensions } = resourcePreviews[0][0]; - let { added, removed, updated, remote, localChange, remoteChange } = resourcePreviews[0][1]; + let { local, remote, localChange, remoteChange } = resourcePreviews[0][1]; if (localChange === Change.None && remoteChange === Change.None) { this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing extensions.`); @@ -269,22 +258,22 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse if (localChange !== Change.None) { await this.backupLocal(JSON.stringify(localExtensions)); - skippedExtensions = await this.updateLocalExtensions(added, removed, updated, skippedExtensions); + skippedExtensions = await this.updateLocalExtensions(local.added, local.removed, local.updated, skippedExtensions); } if (remote) { // update remote this.logService.trace(`${this.syncResourceLogLabel}: Updating remote extensions...`); - const content = JSON.stringify(remote); + const content = JSON.stringify(remote.all); remoteUserData = await this.updateRemoteUserData(content, force ? null : remoteUserData.ref); - this.logService.info(`${this.syncResourceLogLabel}: Updated remote extensions`); + this.logService.info(`${this.syncResourceLogLabel}: Updated remote extensions.${remote.added.length ? ` Added: ${JSON.stringify(remote.added.map(e => e.identifier.id))}.` : ''}${remote.updated.length ? ` Updated: ${JSON.stringify(remote.updated.map(e => e.identifier.id))}.` : ''}${remote.removed.length ? ` Removed: ${JSON.stringify(remote.removed.map(e => e.identifier.id))}.` : ''}`); } if (lastSyncUserData?.ref !== remoteUserData.ref) { // update last sync this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized extensions...`); await this.updateLastSyncUserData(remoteUserData, { skippedExtensions }); - this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized extensions`); + this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized extensions.${skippedExtensions.length ? ` Skipped: ${JSON.stringify(skippedExtensions.map(e => e.identifier.id))}.` : ''}`); } } diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index 39b36a4112..45a89063ba 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -101,8 +101,11 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs ); } - protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { + protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, isRemoteDataFromCurrentMachine: boolean, token: CancellationToken): Promise { const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null; + + // Use remote data as last sync data if last sync data does not exist and remote data is from same machine + lastSyncUserData = lastSyncUserData === null && isRemoteDataFromCurrentMachine ? remoteUserData : lastSyncUserData; const lastSyncGlobalState: IGlobalState | null = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null; const localGloablState = await this.getLocalGlobalState(); diff --git a/src/vs/platform/userDataSync/common/ignoredExtensions.ts b/src/vs/platform/userDataSync/common/ignoredExtensions.ts index 10b5a42af0..ac9ee2d8da 100644 --- a/src/vs/platform/userDataSync/common/ignoredExtensions.ts +++ b/src/vs/platform/userDataSync/common/ignoredExtensions.ts @@ -81,7 +81,7 @@ export class IgnoredExtensionsManagementService implements IIgnoredExtensionsMan return distinct([...defaultIgnoredExtensions, ...added,].filter(setting => removed.indexOf(setting) === -1)); } - private getConfiguredIgnoredExtensions(): string[] { + private getConfiguredIgnoredExtensions(): ReadonlyArray { let userValue = this.configurationService.inspect('settingsSync.ignoredExtensions').userValue; if (userValue !== undefined) { return userValue; diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index 0e2c9eb090..44530bd4a5 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -80,8 +80,11 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem this._register(Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('settingsSync.keybindingsPerPlatform'))(() => this.triggerLocalChange())); } - protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise { + protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, isRemoteDataFromCurrentMachine: boolean, token: CancellationToken): Promise { const remoteContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null; + + // Use remote data as last sync data if last sync data does not exist and remote data is from same machine + lastSyncUserData = lastSyncUserData === null && isRemoteDataFromCurrentMachine ? remoteUserData : lastSyncUserData; const lastSyncContent: string | null = lastSyncUserData ? this.getKeybindingsContentFromLastSyncUserData(lastSyncUserData) : null; // Get file content last to get the latest diff --git a/src/vs/platform/userDataSync/common/settingsMerge.ts b/src/vs/platform/userDataSync/common/settingsMerge.ts index e63b56568f..562e727dc1 100644 --- a/src/vs/platform/userDataSync/common/settingsMerge.ts +++ b/src/vs/platform/userDataSync/common/settingsMerge.ts @@ -21,7 +21,7 @@ export interface IMergeResult { } export function getIgnoredSettings(defaultIgnoredSettings: string[], configurationService: IConfigurationService, settingsContent?: string): string[] { - let value: string[] = []; + let value: ReadonlyArray = []; if (settingsContent) { value = getIgnoredSettingsFromContent(settingsContent); } else { @@ -40,7 +40,7 @@ export function getIgnoredSettings(defaultIgnoredSettings: string[], configurati return distinct([...defaultIgnoredSettings, ...added,].filter(setting => removed.indexOf(setting) === -1)); } -function getIgnoredSettingsFromConfig(configurationService: IConfigurationService): string[] { +function getIgnoredSettingsFromConfig(configurationService: IConfigurationService): ReadonlyArray { let userValue = configurationService.inspect('settingsSync.ignoredSettings').userValue; if (userValue !== undefined) { return userValue; diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index 39fa4c8110..3537f1d6f0 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -69,10 +69,13 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement super(environmentService.settingsResource, SyncResource.Settings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService); } - protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { + protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, isRemoteDataFromCurrentMachine: boolean, token: CancellationToken): Promise { const fileContent = await this.getLocalFileContent(); const formattingOptions = await this.getFormattingOptions(); const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); + + // Use remote data as last sync data if last sync data does not exist and remote data is from same machine + lastSyncUserData = lastSyncUserData === null && isRemoteDataFromCurrentMachine ? remoteUserData : lastSyncUserData; const lastSettingsSyncContent: ISettingsSyncContent | null = lastSyncUserData ? this.getSettingsSyncContent(lastSyncUserData) : null; const ignoredSettings = await this.getIgnoredSettings(); diff --git a/src/vs/platform/userDataSync/common/snippetsSync.ts b/src/vs/platform/userDataSync/common/snippetsSync.ts index e4f7eea21a..86a58ecf2a 100644 --- a/src/vs/platform/userDataSync/common/snippetsSync.ts +++ b/src/vs/platform/userDataSync/common/snippetsSync.ts @@ -52,10 +52,13 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD this._register(Event.filter(this.fileService.onDidFilesChange, e => e.affects(this.snippetsFolder))(() => this.triggerLocalChange())); } - protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { + protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, isRemoteDataFromCurrentMachine: boolean, token: CancellationToken): Promise { const local = await this.getSnippetsFileContents(); const localSnippets = this.toSnippetsContents(local); const remoteSnippets: IStringDictionary | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null; + + // Use remote data as last sync data if last sync data does not exist and remote data is from same machine + lastSyncUserData = lastSyncUserData === null && isRemoteDataFromCurrentMachine ? remoteUserData : lastSyncUserData; const lastSyncSnippets: IStringDictionary | null = lastSyncUserData && lastSyncUserData.syncData ? this.parseSnippets(lastSyncUserData.syncData) : null; if (remoteSnippets) { diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index a19ee293de..93279332d8 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -68,8 +68,8 @@ export class UserDataAutoSyncEnablementService extends Disposable implements _IU return true; case 'off': return false; - default: return this.storageService.getBoolean(enablementKey, StorageScope.GLOBAL, !!defaultEnablement); // {{SQL CARBON EDIT}} strict-null-checks move this to a default case } + return this.storageService.getBoolean(enablementKey, StorageScope.GLOBAL, !!defaultEnablement); */ return false; } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index d55fb5726a..1aa3f8b69d 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -16,7 +16,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import { URI } from 'vs/base/common/uri'; -import { joinPath, isEqualOrParent } from 'vs/base/common/resources'; +import { joinPath, isEqualOrParent, IExtUri } from 'vs/base/common/resources'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { distinct } from 'vs/base/common/arrays'; import { isArray, isString, isObject } from 'vs/base/common/types'; @@ -132,6 +132,10 @@ export const enum SyncResource { } export const ALL_SYNC_RESOURCES: SyncResource[] = [SyncResource.Settings, SyncResource.Keybindings, SyncResource.Snippets, SyncResource.Extensions, SyncResource.GlobalState]; +export function getLastSyncResourceUri(syncResource: SyncResource, environmentService: IEnvironmentService, extUri: IExtUri): URI { + return extUri.joinPath(environmentService.userDataSyncHome, syncResource, `lastSync${syncResource}.json`); +} + export interface IUserDataManifest { latest?: Record session: string; diff --git a/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts b/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts index 448f830380..2e7f3a16c8 100644 --- a/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts @@ -18,10 +18,10 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, null, null, [], []); - assert.deepStrictEqual(actual.added, []); - assert.deepStrictEqual(actual.removed, []); - assert.deepStrictEqual(actual.updated, []); - assert.deepStrictEqual(actual.remote, localExtensions); + assert.deepStrictEqual(actual.local.added, []); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, localExtensions); }); test('merge returns local extension if remote does not exist with ignored extensions', () => { @@ -37,10 +37,10 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, null, null, [], ['a']); - assert.deepStrictEqual(actual.added, []); - assert.deepStrictEqual(actual.removed, []); - assert.deepStrictEqual(actual.updated, []); - assert.deepStrictEqual(actual.remote, expected); + assert.deepStrictEqual(actual.local.added, []); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, expected); }); test('merge returns local extension if remote does not exist with ignored extensions (ignore case)', () => { @@ -56,10 +56,10 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, null, null, [], ['A']); - assert.deepStrictEqual(actual.added, []); - assert.deepStrictEqual(actual.removed, []); - assert.deepStrictEqual(actual.updated, []); - assert.deepStrictEqual(actual.remote, expected); + assert.deepStrictEqual(actual.local.added, []); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, expected); }); test('merge returns local extension if remote does not exist with skipped extensions', () => { @@ -79,10 +79,10 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, null, null, skippedExtension, []); - assert.deepStrictEqual(actual.added, []); - assert.deepStrictEqual(actual.removed, []); - assert.deepStrictEqual(actual.updated, []); - assert.deepStrictEqual(actual.remote, expected); + assert.deepStrictEqual(actual.local.added, []); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, expected); }); test('merge returns local extension if remote does not exist with skipped and ignored extensions', () => { @@ -101,10 +101,10 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, null, null, skippedExtension, ['a']); - assert.deepStrictEqual(actual.added, []); - assert.deepStrictEqual(actual.removed, []); - assert.deepStrictEqual(actual.updated, []); - assert.deepStrictEqual(actual.remote, expected); + assert.deepStrictEqual(actual.local.added, []); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, expected); }); test('merge local and remote extensions when there is no base', () => { @@ -125,10 +125,10 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, null, [], []); - assert.deepStrictEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]); - assert.deepStrictEqual(actual.removed, []); - assert.deepStrictEqual(actual.updated, []); - assert.deepStrictEqual(actual.remote, expected); + assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, expected); }); test('merge local and remote extensions when there is no base and with ignored extensions', () => { @@ -148,10 +148,10 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, null, [], ['a']); - assert.deepStrictEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]); - assert.deepStrictEqual(actual.removed, []); - assert.deepStrictEqual(actual.updated, []); - assert.deepStrictEqual(actual.remote, expected); + assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, expected); }); test('merge local and remote extensions when remote is moved forwarded', () => { @@ -170,9 +170,9 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []); - assert.deepStrictEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]); - assert.deepStrictEqual(actual.removed, [{ id: 'a', uuid: 'a' }, { id: 'd', uuid: 'd' }]); - assert.deepStrictEqual(actual.updated, []); + assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]); + assert.deepStrictEqual(actual.local.removed, [{ id: 'a', uuid: 'a' }, { id: 'd', uuid: 'd' }]); + assert.deepStrictEqual(actual.local.updated, []); assert.strictEqual(actual.remote, null); }); @@ -193,9 +193,9 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []); - assert.deepStrictEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]); - assert.deepStrictEqual(actual.removed, [{ id: 'a', uuid: 'a' }]); - assert.deepStrictEqual(actual.updated, [{ identifier: { id: 'd', uuid: 'd' }, disabled: true, installed: true, version: '1.0.0' }]); + assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]); + assert.deepStrictEqual(actual.local.removed, [{ id: 'a', uuid: 'a' }]); + assert.deepStrictEqual(actual.local.updated, [{ identifier: { id: 'd', uuid: 'd' }, disabled: true, installed: true, version: '1.0.0' }]); assert.strictEqual(actual.remote, null); }); @@ -215,9 +215,9 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], ['a']); - assert.deepStrictEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]); - assert.deepStrictEqual(actual.removed, [{ id: 'd', uuid: 'd' }]); - assert.deepStrictEqual(actual.updated, []); + assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]); + assert.deepStrictEqual(actual.local.removed, [{ id: 'd', uuid: 'd' }]); + assert.deepStrictEqual(actual.local.updated, []); assert.strictEqual(actual.remote, null); }); @@ -239,9 +239,9 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, []); - assert.deepStrictEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]); - assert.deepStrictEqual(actual.removed, [{ id: 'd', uuid: 'd' }]); - assert.deepStrictEqual(actual.updated, []); + assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]); + assert.deepStrictEqual(actual.local.removed, [{ id: 'd', uuid: 'd' }]); + assert.deepStrictEqual(actual.local.updated, []); assert.strictEqual(actual.remote, null); }); @@ -263,9 +263,9 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, ['b']); - assert.deepStrictEqual(actual.added, [{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]); - assert.deepStrictEqual(actual.removed, [{ id: 'd', uuid: 'd' }]); - assert.deepStrictEqual(actual.updated, []); + assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]); + assert.deepStrictEqual(actual.local.removed, [{ id: 'd', uuid: 'd' }]); + assert.deepStrictEqual(actual.local.updated, []); assert.strictEqual(actual.remote, null); }); @@ -285,10 +285,10 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []); - assert.deepStrictEqual(actual.added, []); - assert.deepStrictEqual(actual.removed, []); - assert.deepStrictEqual(actual.updated, []); - assert.deepStrictEqual(actual.remote, localExtensions); + assert.deepStrictEqual(actual.local.added, []); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, localExtensions); }); test('merge local and remote extensions when local is moved forwarded with disabled extensions', () => { @@ -308,10 +308,10 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []); - assert.deepStrictEqual(actual.added, []); - assert.deepStrictEqual(actual.removed, []); - assert.deepStrictEqual(actual.updated, []); - assert.deepStrictEqual(actual.remote, localExtensions); + assert.deepStrictEqual(actual.local.added, []); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, localExtensions); }); test('merge local and remote extensions when local is moved forwarded with ignored settings', () => { @@ -330,10 +330,10 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], ['b']); - assert.deepStrictEqual(actual.added, []); - assert.deepStrictEqual(actual.removed, []); - assert.deepStrictEqual(actual.updated, []); - assert.deepStrictEqual(actual.remote, [ + assert.deepStrictEqual(actual.local.added, []); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, [ { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }, ]); }); @@ -362,10 +362,10 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, []); - assert.deepStrictEqual(actual.added, []); - assert.deepStrictEqual(actual.removed, []); - assert.deepStrictEqual(actual.updated, []); - assert.deepStrictEqual(actual.remote, expected); + assert.deepStrictEqual(actual.local.added, []); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, expected); }); test('merge local and remote extensions when local is moved forwarded with skipped and ignored extensions', () => { @@ -391,10 +391,10 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, ['c']); - assert.deepStrictEqual(actual.added, []); - assert.deepStrictEqual(actual.removed, []); - assert.deepStrictEqual(actual.updated, []); - assert.deepStrictEqual(actual.remote, expected); + assert.deepStrictEqual(actual.local.added, []); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, expected); }); test('merge local and remote extensions when both moved forwarded', () => { @@ -420,10 +420,10 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []); - assert.deepStrictEqual(actual.added, [{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' }]); - assert.deepStrictEqual(actual.removed, [{ id: 'a', uuid: 'a' }]); - assert.deepStrictEqual(actual.updated, []); - assert.deepStrictEqual(actual.remote, expected); + assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' }]); + assert.deepStrictEqual(actual.local.removed, [{ id: 'a', uuid: 'a' }]); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, expected); }); test('merge local and remote extensions when both moved forwarded with ignored extensions', () => { @@ -449,10 +449,10 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], ['a', 'e']); - assert.deepStrictEqual(actual.added, []); - assert.deepStrictEqual(actual.removed, []); - assert.deepStrictEqual(actual.updated, []); - assert.deepStrictEqual(actual.remote, expected); + assert.deepStrictEqual(actual.local.added, []); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, expected); }); test('merge local and remote extensions when both moved forwarded with skipped extensions', () => { @@ -480,10 +480,10 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, []); - assert.deepStrictEqual(actual.added, [{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' }]); - assert.deepStrictEqual(actual.removed, []); - assert.deepStrictEqual(actual.updated, []); - assert.deepStrictEqual(actual.remote, expected); + assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' }]); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, expected); }); test('merge local and remote extensions when both moved forwarded with skipped and ignoredextensions', () => { @@ -511,10 +511,10 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, ['e']); - assert.deepStrictEqual(actual.added, []); - assert.deepStrictEqual(actual.removed, []); - assert.deepStrictEqual(actual.updated, []); - assert.deepStrictEqual(actual.remote, expected); + assert.deepStrictEqual(actual.local.added, []); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, expected); }); test('merge when remote extension has no uuid and different extension id case', () => { @@ -536,10 +536,10 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, null, [], []); - assert.deepStrictEqual(actual.added, [{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' }]); - assert.deepStrictEqual(actual.removed, []); - assert.deepStrictEqual(actual.updated, []); - assert.deepStrictEqual(actual.remote, expected); + assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' }]); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, expected); }); test('merge when remote extension is not an installed extension', () => { @@ -553,9 +553,9 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, null, [], []); - assert.deepStrictEqual(actual.added, []); - assert.deepStrictEqual(actual.removed, []); - assert.deepStrictEqual(actual.updated, []); + assert.deepStrictEqual(actual.local.added, []); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); assert.deepStrictEqual(actual.remote, null); }); @@ -569,10 +569,10 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, null, [], []); - assert.deepStrictEqual(actual.added, []); - assert.deepStrictEqual(actual.removed, []); - assert.deepStrictEqual(actual.updated, []); - assert.deepStrictEqual(actual.remote, localExtensions); + assert.deepStrictEqual(actual.local.added, []); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, localExtensions); }); test('merge when an extension is not an installed extension remotely and does not exist locally', () => { @@ -586,9 +586,9 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, remoteExtensions, [], []); - assert.deepStrictEqual(actual.added, []); - assert.deepStrictEqual(actual.removed, []); - assert.deepStrictEqual(actual.updated, []); + assert.deepStrictEqual(actual.local.added, []); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); assert.deepStrictEqual(actual.remote, null); }); @@ -605,10 +605,10 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, remoteExtensions, [], []); - assert.deepStrictEqual(actual.added, []); - assert.deepStrictEqual(actual.removed, []); - assert.deepStrictEqual(actual.updated, []); - assert.deepStrictEqual(actual.remote, expected); + assert.deepStrictEqual(actual.local.added, []); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, expected); }); test('merge when an extension is an installed extension remotely but not locally and updated remotely', () => { @@ -621,9 +621,9 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, localExtensions, [], []); - assert.deepStrictEqual(actual.added, []); - assert.deepStrictEqual(actual.removed, []); - assert.deepStrictEqual(actual.updated, remoteExtensions); + assert.deepStrictEqual(actual.local.added, []); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, remoteExtensions); assert.deepStrictEqual(actual.remote, null); }); @@ -641,10 +641,10 @@ suite('ExtensionsMerge', () => { const actual = merge(localExtensions, remoteExtensions, null, [], []); - assert.deepStrictEqual(actual.added, []); - assert.deepStrictEqual(actual.removed, []); - assert.deepStrictEqual(actual.updated, []); - assert.deepStrictEqual(actual.remote, expected); + assert.deepStrictEqual(actual.local.added, []); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, expected); }); }); diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index 6fba795e94..396efc1ac2 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -53,7 +53,7 @@ class TestSynchroniser extends AbstractSynchroniser { return super.doSync(remoteUserData, lastSyncUserData, apply); } - protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { + protected override async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, isRemoteDataFromCurrentMachine: boolean, token: CancellationToken): Promise { if (this.syncResult.hasError) { throw new Error('failed'); } diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index f5e51f4504..31360b2765 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -56,8 +56,7 @@ export interface IOpenedWindow { readonly dirty: boolean; } -export interface IOpenEmptyWindowOptions extends IBaseOpenWindowsOptions { -} +export interface IOpenEmptyWindowOptions extends IBaseOpenWindowsOptions { } export type IWindowOpenable = IWorkspaceToOpen | IFolderToOpen | IFileToOpen; @@ -233,6 +232,31 @@ export interface IOSConfiguration { readonly hostname: string; } +export interface IPartsSplash { + baseTheme: string; + colorInfo: { + background: string; + foreground: string | undefined; + editorBackground: string | undefined; + titleBarBackground: string | undefined; + activityBarBackground: string | undefined; + sideBarBackground: string | undefined; + statusBarBackground: string | undefined; + statusBarNoFolderBackground: string | undefined; + windowBorder: string | undefined; + } + layoutInfo: { + sideBarSide: string; + editorPartMinWidth: number; + titleBarHeight: number; + activityBarWidth: number; + sideBarWidth: number; + statusBarHeight: number; + windowBorder: boolean; + windowBorderRadius: string | undefined; + } | undefined +} + export interface INativeWindowConfiguration extends IWindowConfiguration, NativeParsedArgs, ISandboxConfiguration { mainPid: number; @@ -245,7 +269,7 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native tmpDir: string; userDataDir: string; - partsSplashPath: string; + partsSplash?: IPartsSplash; workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; diff --git a/src/vs/platform/windows/electron-main/window.ts b/src/vs/platform/windows/electron-main/window.ts index 8f3714b379..b4ca0acfd0 100644 --- a/src/vs/platform/windows/electron-main/window.ts +++ b/src/vs/platform/windows/electron-main/window.ts @@ -8,7 +8,7 @@ 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, Event, RenderProcessGoneDetails, WebFrameMain } from 'electron'; +import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display, TouchBarSegmentedControl, NativeImage, BrowserWindowConstructorOptions, SegmentedControlSegment, Event, WebFrameMain } 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'; @@ -186,7 +186,6 @@ export class CodeWindow extends Disposable implements ICodeWindow { [`--vscode-window-config=${this.configObjectUrl.resource.toString()}`], v8CacheOptions: browserCodeLoadingCacheStrategy, enableWebSQL: false, - enableRemoteModule: false, spellcheck: false, nativeWindowOpen: true, webviewTag: true, @@ -207,9 +206,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { }; if (browserCodeLoadingCacheStrategy) { - this.logService.info(`window#ctor: using vscode-file:// protocol and V8 cache options: ${browserCodeLoadingCacheStrategy}`); + this.logService.info(`window: using vscode-file:// protocol and V8 cache options: ${browserCodeLoadingCacheStrategy}`); } else { - this.logService.trace(`window#ctor: vscode-file:// protocol is explicitly disabled`); + this.logService.info(`window: vscode-file:// protocol is explicitly disabled`); } // Apply icon to window @@ -420,7 +419,16 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Crashes & Unresponsive & Failed to load this._win.on('unresponsive', () => this.onWindowError(WindowError.UNRESPONSIVE)); this._win.webContents.on('render-process-gone', (event, details) => this.onWindowError(WindowError.CRASHED, details)); - this._win.webContents.on('did-fail-load', (event, errorCode, errorDescription) => this.onWindowError(WindowError.LOAD, errorDescription)); + this._win.webContents.on('did-fail-load', (event, exitCode, reason) => this.onWindowError(WindowError.LOAD, { reason, exitCode })); + + // Prevent windows/iframes from blocking the unload + // through DOM events. We have our own logic for + // unloading a window that should not be confused + // with the DOM way. + // (https://github.com/microsoft/vscode/issues/122736) + this._win.webContents.on('will-prevent-unload', event => { + event.preventDefault(); + }); // Window close this._win.on('closed', () => { @@ -430,7 +438,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { }); // Block all SVG requests from unsupported origins - const supportedSvgSchemes = new Set([Schemas.file, Schemas.vscodeFileResource, Schemas.vscodeRemoteResource, 'devtools']); // TODO: handle webview origin + const supportedSvgSchemes = new Set([Schemas.file, Schemas.vscodeFileResource, Schemas.vscodeRemoteResource, 'devtools']); // TODO@mjbvz: handle webview origin // But allow them if the are made from inside an webview const isSafeFrame = (requestFrame: WebFrameMain | undefined): boolean => { @@ -442,13 +450,16 @@ export class CodeWindow extends Disposable implements ICodeWindow { return false; }; + const isRequestFromSafeContext = (details: Electron.OnBeforeRequestListenerDetails | Electron.OnHeadersReceivedListenerDetails): boolean => { + return details.resourceType === 'xhr' || isSafeFrame(details.frame); + }; + this._win.webContents.session.webRequest.onBeforeRequest((details, callback) => { const uri = URI.parse(details.url); if (uri.path.endsWith('.svg')) { const isSafeResourceUrl = supportedSvgSchemes.has(uri.scheme) || uri.path.includes(Schemas.vscodeRemoteResource); if (!isSafeResourceUrl) { - const isSafeContext = isSafeFrame(details.frame); - return callback({ cancel: !isSafeContext }); + return callback({ cancel: !isRequestFromSafeContext(details) }); } } @@ -474,8 +485,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // remote extension schemes have the following format // http://127.0.0.1:/vscode-remote-resource?path= if (!uri.path.includes(Schemas.vscodeRemoteResource) && contentTypes.some(contentType => contentType.toLowerCase().includes('image/svg'))) { - const isSafeContext = isSafeFrame(details.frame); - return callback({ cancel: !isSafeContext }); + return callback({ cancel: !isRequestFromSafeContext(details) }); } } @@ -541,19 +551,19 @@ export class CodeWindow extends Disposable implements ICodeWindow { } private async onWindowError(error: WindowError.UNRESPONSIVE): Promise; - private async onWindowError(error: WindowError.CRASHED, details: RenderProcessGoneDetails): Promise; - private async onWindowError(error: WindowError.LOAD, details: string): Promise; - private async onWindowError(type: WindowError, details?: string | RenderProcessGoneDetails): Promise { + private async onWindowError(error: WindowError.CRASHED, details: { reason: string, exitCode: number }): Promise; + private async onWindowError(error: WindowError.LOAD, details: { reason: string, exitCode: number }): Promise; + private async onWindowError(type: WindowError, details?: { reason: string, exitCode: number }): Promise { switch (type) { case WindowError.CRASHED: - this.logService.error(`CodeWindow: renderer process crashed (detail: ${typeof details === 'string' ? details : details?.reason})`); + this.logService.error(`CodeWindow: renderer process crashed (reason: ${details?.reason || ''}, code: ${details?.exitCode || ''})`); break; case WindowError.UNRESPONSIVE: this.logService.error('CodeWindow: detected unresponsive'); break; case WindowError.LOAD: - this.logService.error(`CodeWindow: failed to load workbench window: ${typeof details === 'string' ? details : details?.reason}`); + this.logService.error(`CodeWindow: failed to load workbench window (reason: ${details?.reason || ''}, code: ${details?.exitCode || ''})`); break; } @@ -569,12 +579,14 @@ export class CodeWindow extends Disposable implements ICodeWindow { type WindowErrorClassification = { type: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true }; reason: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true }; + code: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true }; }; type WindowErrorEvent = { type: WindowError; reason: string | undefined; + code: number | undefined; }; - this.telemetryService.publicLog2('windowerror', { type, reason: typeof details !== 'string' ? details?.reason : undefined }); + this.telemetryService.publicLog2('windowerror', { type, reason: details?.reason, code: details?.exitCode }); // Unresponsive if (type === WindowError.UNRESPONSIVE) { @@ -613,10 +625,10 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Crashed else if (type === WindowError.CRASHED) { let message: string; - if (typeof details === 'string' || !details) { + if (!details) { message = localize('appCrashed', "The window has crashed"); } else { - message = localize('appCrashedDetails', "The window has crashed (reason: '{0}')", details.reason); + message = localize('appCrashedDetails', "The window has crashed (reason: '{0}', code: '{1}')", details.reason, details.exitCode ?? ''); } const result = await this.dialogMainService.showMessageBox({ @@ -641,8 +653,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { } private destroyWindow(): void { - this._onDidDestroy.fire(); // 'close' event will not be fired on destroy(), so signal crash via explicit event - this._win.destroy(); // make sure to destroy the window as it has crashed + this._onDidDestroy.fire(); // 'close' event will not be fired on destroy(), so signal crash via explicit event + this._win.destroy(); // make sure to destroy the window as it has crashed } private onDidDeleteUntitledWorkspace(workspace: IWorkspaceIdentifier): void { @@ -779,6 +791,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Update window related properties configuration.fullscreen = this.isFullScreen; configuration.maximized = this._win.isMaximized(); + configuration.partsSplash = this.themeMainService.getWindowSplash(); // Update with latest perf marks mark('code/willOpenNewWindow'); diff --git a/src/vs/platform/windows/electron-main/windowsFinder.ts b/src/vs/platform/windows/electron-main/windowsFinder.ts index c2b4ad1c9b..601f5590a3 100644 --- a/src/vs/platform/windows/electron-main/windowsFinder.ts +++ b/src/vs/platform/windows/electron-main/windowsFinder.ts @@ -8,7 +8,7 @@ import { IWorkspaceIdentifier, IResolvedWorkspace, isWorkspaceIdentifier, isSing 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 { +export function findWindowOnFile(windows: ICodeWindow[], fileUri: URI, localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | undefined): ICodeWindow | undefined { // First check for windows with workspaces that have a parent folder of the provided path opened for (const window of windows) { diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 8f53e10645..390b1050a4 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -14,7 +14,7 @@ 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 { IStateMainService } from 'vs/platform/state/electron-main/state'; import { CodeWindow } from 'vs/platform/windows/electron-main/window'; import { app, BrowserWindow, MessageBoxOptions, nativeTheme, WebContents } from 'electron'; import { ILifecycleMainService, UnloadReason } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; @@ -139,13 +139,13 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic private readonly _onDidChangeWindowsCount = this._register(new Emitter()); readonly onDidChangeWindowsCount = this._onDidChangeWindowsCount.event; - private readonly windowsStateHandler = this._register(new WindowsStateHandler(this, this.stateService, this.lifecycleMainService, this.logService, this.configurationService)); + private readonly windowsStateHandler = this._register(new WindowsStateHandler(this, this.stateMainService, this.lifecycleMainService, this.logService, this.configurationService)); constructor( private readonly machineId: string, private readonly initialUserEnv: IProcessEnvironment, @ILogService private readonly logService: ILogService, - @IStateService private readonly stateService: IStateService, + @IStateMainService private readonly stateMainService: IStateMainService, @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @IBackupMainService private readonly backupMainService: IBackupMainService, @@ -409,7 +409,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic 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); + windowToUseForFiles = findWindowOnFile(windows, fileToCheck.fileUri, workspace => workspace.configPath.scheme === Schemas.file ? this.workspacesManagementMainService.resolveLocalWorkspaceSync(workspace.configPath) : undefined); } if (!windowToUseForFiles) { @@ -856,6 +856,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic if (isFileToOpen(openable)) { options = { ...options, forceOpenWorkspaceAsFile: true }; } + return this.doResolveFilePath(uri.fsPath, options); } @@ -1168,8 +1169,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic appRoot: this.environmentMainService.appRoot, execPath: process.execPath, - nodeCachedDataDir: this.environmentMainService.nodeCachedDataDir, - partsSplashPath: join(this.environmentMainService.userDataPath, 'rapid_render.json'), + codeCachePath: this.environmentMainService.codeCachePath, // If we know the backup folder upfront (for empty windows to restore), we can set it // directly here which helps for restoring UI state associated with that window. // For all other cases we first call into registerEmptyWindowBackupSync() to set it before diff --git a/src/vs/platform/windows/electron-main/windowsStateHandler.ts b/src/vs/platform/windows/electron-main/windowsStateHandler.ts index 7745467e39..483b793d79 100644 --- a/src/vs/platform/windows/electron-main/windowsStateHandler.ts +++ b/src/vs/platform/windows/electron-main/windowsStateHandler.ts @@ -11,7 +11,7 @@ import { URI } from 'vs/base/common/uri'; 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 { IStateMainService } from 'vs/platform/state/electron-main/state'; import { INativeWindowConfiguration, IWindowSettings } from 'vs/platform/windows/common/windows'; import { defaultWindowState, ICodeWindow, IWindowsMainService, IWindowState as IWindowUIState, WindowMode } from 'vs/platform/windows/electron-main/windows'; import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; @@ -53,7 +53,7 @@ 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 readonly _state = restoreWindowsState(this.stateMainService.getItem(WindowsStateHandler.windowsStateStorageKey)); private lastClosedState: IWindowState | undefined = undefined; @@ -61,7 +61,7 @@ export class WindowsStateHandler extends Disposable { constructor( @IWindowsMainService private readonly windowsMainService: IWindowsMainService, - @IStateService private readonly stateService: IStateService, + @IStateMainService private readonly stateMainService: IStateMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @ILogService private readonly logService: ILogService, @IConfigurationService private readonly configurationService: IConfigurationService @@ -177,7 +177,7 @@ export class WindowsStateHandler extends Disposable { // Persist const state = getWindowsStateStoreData(currentWindowsState); - this.stateService.setItem(WindowsStateHandler.windowsStateStorageKey, state); + this.stateMainService.setItem(WindowsStateHandler.windowsStateStorageKey, state); if (this.shuttingDown) { this.logService.trace('[WindowsStateHandler] onBeforeShutdown', state); @@ -232,9 +232,11 @@ export class WindowsStateHandler extends Disposable { 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 + // or to address a Electron issue on macOS (https://github.com/microsoft/vscode/issues/125122) let allowFullscreen: boolean; if (state.hasDefaultState) { - allowFullscreen = !!(windowConfig?.newWindowDimensions && ['fullscreen', 'inherit', 'offset'].indexOf(windowConfig.newWindowDimensions) >= 0); + const configAllowsFullScreen = !!(windowConfig?.newWindowDimensions && ['fullscreen', 'inherit', 'offset'].indexOf(windowConfig.newWindowDimensions) >= 0); + allowFullscreen = configAllowsFullScreen || (isMacintosh && windowConfig?.nativeFullScreen !== false); } // Window state is from a previous session: only allow fullscreen when we got updated or user wants to restore @@ -337,14 +339,22 @@ export class WindowsStateHandler extends Disposable { // 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(); + let state: INewWindowState = 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) { + + // TODO@electron macOS: if the current window is fullscreen and native fullscreen + // is not disabled, always open a new window in fullscreen. This is a workaround + // for regression https://github.com/microsoft/vscode/issues/125122 + if (isMacintosh && windowConfig?.nativeFullScreen !== false && lastActive?.isFullScreen) { + state.mode = WindowMode.Fullscreen; + } + + // Adjust according to `newWindowDimensions` user setting + else if (windowConfig?.newWindowDimensions) { if (windowConfig.newWindowDimensions === 'maximized') { state.mode = WindowMode.Maximized; ensureNoOverlap = false; @@ -367,7 +377,7 @@ export class WindowsStateHandler extends Disposable { state = this.ensureNoOverlap(state); } - (state as INewWindowState).hasDefaultState = true; // flag as default state + state.hasDefaultState = true; // flag as default state return state; } diff --git a/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts b/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts index 6d132f311b..b5fd7fad43 100644 --- a/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts +++ b/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts @@ -28,7 +28,7 @@ suite('WindowsFinder', () => { }; 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; }; + const localWorkspaceResolver = (workspace: any) => { return workspace === testWorkspace ? { id: testWorkspace.id, configPath: workspace.configPath, folders: testWorkspaceFolders } : undefined; }; function createTestCodeWindow(options: { lastFocusTime: number, openedFolderUri?: URI, openedWorkspace?: IWorkspaceIdentifier }): ICodeWindow { return new class implements ICodeWindow { diff --git a/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts b/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts new file mode 100644 index 0000000000..a29072e455 --- /dev/null +++ b/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts @@ -0,0 +1,201 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { tmpdir } from 'os'; +import { join } from 'vs/base/common/path'; +import { restoreWindowsState, getWindowsStateStoreData, IWindowsState, IWindowState } from 'vs/platform/windows/electron-main/windowsStateHandler'; +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'; + +suite('Windows State Storing', () => { + + 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.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 assertEqualWindowState(expected: IWindowState | undefined, actual: IWindowState | undefined, message?: string) { + if (!expected || !actual) { + assert.deepStrictEqual(expected, actual, message); + return; + } + assert.strictEqual(expected.backupPath, actual.backupPath, message); + assertEqualURI(expected.folderUri, actual.folderUri, message); + assert.strictEqual(expected.remoteAuthority, actual.remoteAuthority, message); + assertEqualWorkspace(expected.workspace, actual.workspace, message); + assert.deepStrictEqual(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.strictEqual(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 = join(tmpdir(), 'windowStateTest', 'backupFolder1'); + const testBackupPath2 = join(tmpdir(), 'windowStateTest', 'backupFolder2'); + + const testWSPath = URI.file(join(tmpdir(), 'windowStateTest', 'test.code-workspace')); + const testFolderURI = URI.file(join(tmpdir(), 'windowStateTest', 'testFolder')); + + const testRemoteFolderURI = URI.parse('foo://bar/c/d'); + + 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_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/workspace/common/workspaceTrust.ts b/src/vs/platform/workspace/common/workspaceTrust.ts index 1a303f1482..624e338a1e 100644 --- a/src/vs/platform/workspace/common/workspaceTrust.ts +++ b/src/vs/platform/workspace/common/workspaceTrust.ts @@ -1,9 +1,10 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -29,27 +30,43 @@ export interface WorkspaceTrustRequestButton { export interface WorkspaceTrustRequestOptions { readonly buttons?: WorkspaceTrustRequestButton[]; readonly message?: string; - readonly modal: boolean; } -export type WorkspaceTrustChangeEvent = Event; export const IWorkspaceTrustManagementService = createDecorator('workspaceTrustManagementService'); export interface IWorkspaceTrustManagementService { readonly _serviceBrand: undefined; - onDidChangeTrust: WorkspaceTrustChangeEvent; + onDidChangeTrust: Event; onDidChangeTrustedFolders: Event; + readonly workspaceTrustEnabled: boolean; + readonly workspaceResolved: Promise; + readonly workspaceTrustInitialized: Promise; + acceptsOutOfWorkspaceFiles: boolean; + isWorkpaceTrusted(): boolean; + isWorkspaceTrustForced(): boolean; + canSetParentFolderTrust(): boolean; - setParentFolderTrust(trusted: boolean): void; + setParentFolderTrust(trusted: boolean): Promise; + canSetWorkspaceTrust(): boolean; - setWorkspaceTrust(trusted: boolean): void; - getFolderTrustInfo(folder: URI): IWorkspaceTrustUriInfo; - setFoldersTrust(folders: URI[], trusted: boolean): void; - getTrustedFolders(): URI[]; - setTrustedFolders(folders: URI[]): void; + setWorkspaceTrust(trusted: boolean): Promise; + + getUriTrustInfo(uri: URI): Promise; + setUrisTrust(uri: URI[], trusted: boolean): Promise; + + getTrustedUris(): URI[]; + setTrustedUris(uris: URI[]): Promise; + + addWorkspaceTrustTransitionParticipant(participant: IWorkspaceTrustTransitionParticipant): IDisposable; +} + +export const enum WorkspaceTrustUriResponse { + Open = 1, + OpenInNewWindow = 2, + Cancel = 3 } export const IWorkspaceTrustRequestService = createDecorator('workspaceTrustRequestService'); @@ -57,14 +74,18 @@ export const IWorkspaceTrustRequestService = createDecorator; - readonly onDidCompleteWorkspaceTrustRequest: Event; + readonly onDidInitiateWorkspaceTrustRequest: Event; + requestOpenUris(uris: URI[]): Promise; cancelRequest(): void; - completeRequest(trusted?: boolean): void; + completeRequest(trusted?: boolean): Promise; requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise; } +export interface IWorkspaceTrustTransitionParticipant { + participate(trusted: boolean): Promise; +} + export interface IWorkspaceTrustUriInfo { uri: URI, trusted: boolean diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index 8a90f7f5d0..497d0a2da6 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -39,7 +39,7 @@ export interface IWorkspacesService { readonly _serviceBrand: undefined; // Workspaces Management - enterWorkspace(path: URI): Promise; + enterWorkspace(path: URI): Promise; createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise; deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise; getWorkspaceIdentifier(workspacePath: URI): Promise; @@ -320,7 +320,7 @@ export function toWorkspaceFolders(configuredFolders: IStoredWorkspaceFolder[], const relativeTo = extUri.dirname(workspaceConfigFile); for (let configuredFolder of configuredFolders) { - let uri: URI | null = null; + let uri: URI | undefined = undefined; if (isRawFileWorkspaceFolder(configuredFolder)) { if (configuredFolder.path) { uri = extUri.resolvePath(relativeTo, configuredFolder.path); @@ -328,16 +328,16 @@ export function toWorkspaceFolders(configuredFolders: IStoredWorkspaceFolder[], } 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 }); + uri = uri.with({ path: '/' + uri.path }); // this makes sure all workspace folder are absolute } } catch (e) { - console.warn(e); - // ignore + console.warn(e); // ignore } } + if (uri) { + // remove duplicates let comparisonKey = extUri.getComparisonKey(uri); if (!seen.has(comparisonKey)) { @@ -369,11 +369,9 @@ export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, 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 - absolute = false; + absolute = false; // if it was an untitled workspace, try to make paths relative } else { - // for existing workspaces, preserve whether a path was absolute or relative - absolute = !isRawFileWorkspaceFolder(folder) || isAbsolute(folder.path); + absolute = !isRawFileWorkspaceFolder(folder) || isAbsolute(folder.path); // for existing workspaces, preserve whether a path was absolute or relative } rewrittenFolders.push(getStoredWorkspaceFolder(folderURI, absolute, folder.name, targetConfigFolder, slashForPath, extUri)); } diff --git a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts index 57818b3ca3..fd2ba56335 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts @@ -5,7 +5,7 @@ import { localize } from 'vs/nls'; import { coalesce } from 'vs/base/common/arrays'; -import { IStateService } from 'vs/platform/state/node/state'; +import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { app, JumpListCategory, JumpListItem } from 'electron'; import { ILogService } from 'vs/platform/log/common/log'; import { normalizeDriveLetter, splitName } from 'vs/base/common/labels'; @@ -17,7 +17,7 @@ import { ThrottledDelayer } from 'vs/base/common/async'; import { originalFSPath, basename, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; -import { exists } from 'vs/base/node/pfs'; +import { Promises } from 'vs/base/node/pfs'; import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -62,7 +62,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa private readonly macOSRecentDocumentsUpdater = this._register(new ThrottledDelayer(800)); constructor( - @IStateService private readonly stateService: IStateService, + @IStateMainService private readonly stateMainService: IStateMainService, @ILogService private readonly logService: ILogService, @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService @@ -190,7 +190,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa const loc = location(mru.workspaces[i]); if (loc.scheme === Schemas.file) { const workspacePath = originalFSPath(loc); - if (await exists(workspacePath)) { + if (await Promises.exists(workspacePath)) { workspaceEntries.push(workspacePath); entries++; } @@ -210,7 +210,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa continue; } - if (await exists(filePath)) { + if (await Promises.exists(filePath)) { fileEntries.push(filePath); entries++; } @@ -291,7 +291,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa } private getRecentlyOpenedFromStorage(): IRecentlyOpened { - const storedRecents = this.stateService.getItem(WorkspacesHistoryMainService.recentlyOpenedStorageKey); + const storedRecents = this.stateMainService.getItem(WorkspacesHistoryMainService.recentlyOpenedStorageKey); return restoreRecentlyOpened(storedRecents, this.logService); } @@ -299,7 +299,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa private saveRecentlyOpened(recent: IRecentlyOpened): void { const serialized = toStoreData(recent); - this.stateService.setItem(WorkspacesHistoryMainService.recentlyOpenedStorageKey, serialized); + this.stateMainService.setItem(WorkspacesHistoryMainService.recentlyOpenedStorageKey, serialized); } updateWindowsJumpList(): void { diff --git a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts index 764954a23b..c049b9ef15 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts @@ -25,13 +25,13 @@ export class WorkspacesMainService implements AddFirstParameterToFunctions { + 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; + return undefined; } createUntitledWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise { diff --git a/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts index a868c24751..a30095df7d 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts @@ -6,8 +6,8 @@ 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 { rimrafSync, readdirSync, writeFileSync, Promises } from 'vs/base/node/pfs'; +import { 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'; @@ -41,7 +41,7 @@ export interface IWorkspacesManagementMainService { readonly onDidDeleteUntitledWorkspace: Event; readonly onDidEnterWorkspace: Event; - enterWorkspace(intoWindow: ICodeWindow, openedWindows: ICodeWindow[], path: URI): Promise; + enterWorkspace(intoWindow: ICodeWindow, openedWindows: ICodeWindow[], path: URI): Promise; createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise; createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier; @@ -52,7 +52,7 @@ export interface IWorkspacesManagementMainService { getUntitledWorkspacesSync(): IUntitledWorkspaceInfo[]; isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean; - resolveLocalWorkspaceSync(path: URI): IResolvedWorkspace | null; + resolveLocalWorkspaceSync(path: URI): IResolvedWorkspace | undefined; getWorkspaceIdentifier(workspacePath: URI): Promise; } @@ -83,20 +83,20 @@ export class WorkspacesManagementMainService extends Disposable implements IWork super(); } - resolveLocalWorkspaceSync(uri: URI): IResolvedWorkspace | null { + resolveLocalWorkspaceSync(uri: URI): IResolvedWorkspace | undefined { if (!this.isWorkspacePath(uri)) { - return null; // does not look like a valid workspace config file + return undefined; // does not look like a valid workspace config file } if (uri.scheme !== Schemas.file) { - return null; + return undefined; } let contents: string; try { contents = readFileSync(uri.fsPath, 'utf8'); } catch (error) { - return null; // invalid workspace + return undefined; // invalid workspace } return this.doResolveWorkspace(uri, contents); @@ -106,7 +106,7 @@ export class WorkspacesManagementMainService extends Disposable implements IWork return isUntitledWorkspace(uri, this.environmentMainService) || hasWorkspaceFileExtension(uri); } - private doResolveWorkspace(path: URI, contents: string): IResolvedWorkspace | null { + private doResolveWorkspace(path: URI, contents: string): IResolvedWorkspace | undefined { try { const workspace = this.doParseStoredWorkspace(path, contents); const workspaceIdentifier = getWorkspaceIdentifier(path); @@ -120,7 +120,7 @@ export class WorkspacesManagementMainService extends Disposable implements IWork this.logService.warn(error.toString()); } - return null; + return undefined; } private doParseStoredWorkspace(path: URI, contents: string): IStoredWorkspace { @@ -142,8 +142,8 @@ export class WorkspacesManagementMainService extends Disposable implements IWork const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders, remoteAuthority); const configPath = workspace.configPath.fsPath; - await promises.mkdir(dirname(configPath), { recursive: true }); - await writeFile(configPath, JSON.stringify(storedWorkspace, null, '\t')); + await Promises.mkdir(dirname(configPath), { recursive: true }); + await Promises.writeFile(configPath, JSON.stringify(storedWorkspace, null, '\t')); return workspace; } @@ -238,19 +238,19 @@ export class WorkspacesManagementMainService extends Disposable implements IWork return untitledWorkspaces; } - async enterWorkspace(window: ICodeWindow, windows: ICodeWindow[], path: URI): Promise { + 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 + return undefined; // 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 + return undefined; // return early if the workspace is not valid } const result = this.doEnterWorkspace(window, getWorkspaceIdentifier(path)); if (!result) { - return null; + return undefined; } // Emit as event @@ -287,9 +287,9 @@ export class WorkspacesManagementMainService extends Disposable implements IWork return true; // OK } - private doEnterWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier): IEnterWorkspaceResult | null { + private doEnterWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier): IEnterWorkspaceResult | undefined { if (!window.config) { - return null; + return undefined; } window.focus(); diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts index a42be4846e..29c16148f3 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts @@ -109,13 +109,13 @@ flakySuite('WorkspacesManagementMainService', () => { service = new WorkspacesManagementMainService(environmentMainService, new NullLogService(), new TestBackupMainService(), new TestDialogMainService(), productService); - return fs.promises.mkdir(untitledWorkspacesHomePath, { recursive: true }); + return pfs.Promises.mkdir(untitledWorkspacesHomePath, { recursive: true }); }); teardown(() => { service.dispose(); - return pfs.rimraf(testDir); + return pfs.Promises.rm(testDir); }); function assertPathEquals(p1: string, p2: string): void { diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 1fea4e64fe..1f8ae8e34b 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -24,7 +24,7 @@ declare module 'vscode' { /** * The identifier of the actual command handler. - * @see [commands.registerCommand](#commands.registerCommand). + * @see {@link commands.registerCommand} */ command: string; @@ -43,7 +43,7 @@ declare module 'vscode' { /** * Represents a line of text, such as a line of source code. * - * TextLine objects are __immutable__. When a [document](#TextDocument) changes, + * TextLine objects are __immutable__. When a {@link TextDocument document} changes, * previously retrieved lines will not represent the latest state. */ export interface TextLine { @@ -76,14 +76,14 @@ declare module 'vscode' { /** * Whether this line is whitespace only, shorthand - * for [TextLine.firstNonWhitespaceCharacterIndex](#TextLine.firstNonWhitespaceCharacterIndex) === [TextLine.text.length](#TextLine.text). + * for {@link TextLine.firstNonWhitespaceCharacterIndex} === {@link TextLine.text TextLine.text.length}. */ readonly isEmptyOrWhitespace: boolean; } /** * Represents a text document, such as a source file. Text documents have - * [lines](#TextLine) and knowledge about an underlying resource like a file. + * {@link TextLine lines} and knowledge about an underlying resource like a file. */ export interface TextDocument { @@ -93,21 +93,21 @@ declare module 'vscode' { * *Note* that most documents use the `file`-scheme, which means they are files on disk. However, **not** all documents are * saved on disk and therefore the `scheme` must be checked before trying to access the underlying file or siblings on disk. * - * @see [FileSystemProvider](#FileSystemProvider) - * @see [TextDocumentContentProvider](#TextDocumentContentProvider) + * @see {@link FileSystemProvider} + * @see {@link TextDocumentContentProvider} */ readonly uri: Uri; /** * The file system path of the associated resource. Shorthand - * notation for [TextDocument.uri.fsPath](#TextDocument.uri). Independent of the uri scheme. + * notation for {@link TextDocument.uri TextDocument.uri.fsPath}. Independent of the uri scheme. */ readonly fileName: string; /** * Is this document representing an untitled file which has never been saved yet. *Note* that - * this does not mean the document will be saved to disk, use [`uri.scheme`](#Uri.scheme) - * to figure out where a document will be [saved](#FileSystemProvider), e.g. `file`, `ftp` etc. + * this does not mean the document will be saved to disk, use {@link Uri.scheme `uri.scheme`} + * to figure out where a document will be {@link FileSystemProvider saved}, e.g. `file`, `ftp` etc. */ readonly isUntitled: boolean; @@ -143,7 +143,7 @@ declare module 'vscode' { save(): Thenable; /** - * The [end of line](#EndOfLine) sequence that is predominately + * The {@link EndOfLine end of line} sequence that is predominately * used in this document. */ readonly eol: EndOfLine; @@ -159,7 +159,7 @@ declare module 'vscode' { * document are not reflected. * * @param line A line number in [0, lineCount). - * @return A [line](#TextLine). + * @return A {@link TextLine line}. */ lineAt(line: number): TextLine; @@ -168,18 +168,19 @@ declare module 'vscode' { * that the returned object is *not* live and changes to the * document are not reflected. * - * The position will be [adjusted](#TextDocument.validatePosition). + * The position will be {@link TextDocument.validatePosition adjusted}. + * + * @see {@link TextDocument.lineAt} * - * @see [TextDocument.lineAt](#TextDocument.lineAt) * @param position A position. - * @return A [line](#TextLine). + * @return A {@link TextLine line}. */ lineAt(position: Position): TextLine; /** * Converts the position to a zero-based offset. * - * The position will be [adjusted](#TextDocument.validatePosition). + * The position will be {@link TextDocument.validatePosition adjusted}. * * @param position A position. * @return A valid zero-based offset. @@ -190,13 +191,13 @@ declare module 'vscode' { * Converts a zero-based offset to a position. * * @param offset A zero-based offset. - * @return A valid [position](#Position). + * @return A valid {@link Position}. */ positionAt(offset: number): Position; /** * Get the text of this document. A substring can be retrieved by providing - * a range. The range will be [adjusted](#TextDocument.validateRange). + * a range. The range will be {@link TextDocument.validateRange adjusted}. * * @param range Include only the text included by the range. * @return The text inside the provided range or the entire text. @@ -206,16 +207,16 @@ declare module 'vscode' { /** * Get a word-range at the given position. By default words are defined by * common separators, like space, -, _, etc. In addition, per language custom - * [word definitions](#LanguageConfiguration.wordPattern) can be defined. It + * [word definitions} can be defined. It * is also possible to provide a custom regular expression. * * * *Note 1:* A custom regular expression must not match the empty string and * if it does, it will be ignored. * * *Note 2:* A custom regular expression will fail to match multiline strings * and in the name of speed regular expressions should not match words with - * spaces. Use [`TextLine.text`](#TextLine.text) for more complex, non-wordy, scenarios. + * spaces. Use {@link TextLine.text `TextLine.text`} for more complex, non-wordy, scenarios. * - * The position will be [adjusted](#TextDocument.validatePosition). + * The position will be {@link TextDocument.validatePosition adjusted}. * * @param position A position. * @param regex Optional regular expression that describes what a word is. @@ -244,8 +245,8 @@ declare module 'vscode' { * Represents a line and character position, such as * the position of the cursor. * - * Position objects are __immutable__. Use the [with](#Position.with) or - * [translate](#Position.translate) methods to derive new positions + * Position objects are __immutable__. Use the {@link Position.with with} or + * {@link Position.translate translate} methods to derive new positions * from an existing position. */ export class Position { @@ -343,8 +344,8 @@ declare module 'vscode' { /** * Create a new position derived from this position. * - * @param line Value that should be used as line value, default is the [existing value](#Position.line) - * @param character Value that should be used as character value, default is the [existing value](#Position.character) + * @param line Value that should be used as line value, default is the {@link Position.line existing value} + * @param character Value that should be used as character value, default is the {@link Position.character existing value} * @return A position where line and character are replaced by the given values. */ with(line?: number, character?: number): Position; @@ -361,21 +362,21 @@ declare module 'vscode' { /** * A range represents an ordered pair of two positions. - * It is guaranteed that [start](#Range.start).isBeforeOrEqual([end](#Range.end)) + * It is guaranteed that {@link Range.start start}.isBeforeOrEqual({@link Range.end end}) * - * Range objects are __immutable__. Use the [with](#Range.with), - * [intersection](#Range.intersection), or [union](#Range.union) methods + * Range objects are __immutable__. Use the {@link Range.with with}, + * {@link Range.intersection intersection}, or {@link Range.union union} methods * to derive new ranges from an existing range. */ export class Range { /** - * The start position. It is before or equal to [end](#Range.end). + * The start position. It is before or equal to {@link Range.end end}. */ readonly start: Position; /** - * The end position. It is after or equal to [start](#Range.start). + * The end position. It is after or equal to {@link Range.start start}. */ readonly end: Position; @@ -422,7 +423,7 @@ declare module 'vscode' { * Check if `other` equals this range. * * @param other A range. - * @return `true` when start and end are [equal](#Position.isEqual) to + * @return `true` when start and end are {@link Position.isEqual equal} to * start and end of this range. */ isEqual(other: Range): boolean; @@ -448,8 +449,8 @@ declare module 'vscode' { /** * Derived a new range from this range. * - * @param start A position that should be used as start. The default value is the [current start](#Range.start). - * @param end A position that should be used as end. The default value is the [current end](#Range.end). + * @param start A position that should be used as start. The default value is the {@link Range.start current start}. + * @param end A position that should be used as end. The default value is the {@link Range.end current end}. * @return A range derived from this range with the given start and end position. * If start and end are not different `this` range will be returned. */ @@ -472,13 +473,13 @@ declare module 'vscode' { /** * The position at which the selection starts. - * This position might be before or after [active](#Selection.active). + * This position might be before or after {@link Selection.active active}. */ anchor: Position; /** * The position of the cursor. - * This position might be before or after [anchor](#Selection.anchor). + * This position might be before or after {@link Selection.anchor anchor}. */ active: Position; @@ -501,13 +502,13 @@ declare module 'vscode' { constructor(anchorLine: number, anchorCharacter: number, activeLine: number, activeCharacter: number); /** - * A selection is reversed if [active](#Selection.active).isBefore([anchor](#Selection.anchor)). + * A selection is reversed if {@link Selection.active active}.isBefore({@link Selection.anchor anchor}). */ isReversed: boolean; } /** - * Represents sources that can cause [selection change events](#window.onDidChangeTextEditorSelection). + * Represents sources that can cause {@link window.onDidChangeTextEditorSelection selection change events}. */ export enum TextEditorSelectionChangeKind { /** @@ -525,62 +526,62 @@ declare module 'vscode' { } /** - * Represents an event describing the change in a [text editor's selections](#TextEditor.selections). + * Represents an event describing the change in a {@link TextEditor.selections text editor's selections}. */ export interface TextEditorSelectionChangeEvent { /** - * The [text editor](#TextEditor) for which the selections have changed. + * The {@link TextEditor text editor} for which the selections have changed. */ readonly textEditor: TextEditor; /** - * The new value for the [text editor's selections](#TextEditor.selections). + * The new value for the {@link TextEditor.selections text editor's selections}. */ - readonly selections: ReadonlyArray; + readonly selections: readonly Selection[]; /** - * The [change kind](#TextEditorSelectionChangeKind) which has triggered this + * The {@link TextEditorSelectionChangeKind change kind} which has triggered this * event. Can be `undefined`. */ readonly kind?: TextEditorSelectionChangeKind; } /** - * Represents an event describing the change in a [text editor's visible ranges](#TextEditor.visibleRanges). + * Represents an event describing the change in a {@link TextEditor.visibleRanges text editor's visible ranges}. */ export interface TextEditorVisibleRangesChangeEvent { /** - * The [text editor](#TextEditor) for which the visible ranges have changed. + * The {@link TextEditor text editor} for which the visible ranges have changed. */ readonly textEditor: TextEditor; /** - * The new value for the [text editor's visible ranges](#TextEditor.visibleRanges). + * The new value for the {@link TextEditor.visibleRanges text editor's visible ranges}. */ - readonly visibleRanges: ReadonlyArray; + readonly visibleRanges: readonly Range[]; } /** - * Represents an event describing the change in a [text editor's options](#TextEditor.options). + * Represents an event describing the change in a {@link TextEditor.options text editor's options}. */ export interface TextEditorOptionsChangeEvent { /** - * The [text editor](#TextEditor) for which the options have changed. + * The {@link TextEditor text editor} for which the options have changed. */ readonly textEditor: TextEditor; /** - * The new value for the [text editor's options](#TextEditor.options). + * The new value for the {@link TextEditor.options text editor's options}. */ readonly options: TextEditorOptions; } /** - * Represents an event describing the change of a [text editor's view column](#TextEditor.viewColumn). + * Represents an event describing the change of a {@link TextEditor.viewColumn text editor's view column}. */ export interface TextEditorViewColumnChangeEvent { /** - * The [text editor](#TextEditor) for which the view column has changed. + * The {@link TextEditor text editor} for which the view column has changed. */ readonly textEditor: TextEditor; /** - * The new value for the [text editor's view column](#TextEditor.viewColumn). + * The new value for the {@link TextEditor.viewColumn text editor's view column}. */ readonly viewColumn: ViewColumn; } @@ -634,14 +635,14 @@ declare module 'vscode' { } /** - * Represents a [text editor](#TextEditor)'s [options](#TextEditor.options). + * Represents a {@link TextEditor text editor}'s {@link TextEditor.options options}. */ export interface TextEditorOptions { /** * The size in spaces a tab takes. This is used for two purposes: * - the rendering width of a tab character; - * - the number of spaces to insert when [insertSpaces](#TextEditorOptions.insertSpaces) is true. + * - the number of spaces to insert when {@link TextEditorOptions.insertSpaces insertSpaces} is true. * * When getting a text editor's options, this property will always be a number (resolved). * When setting a text editor's options, this property is optional and it can be a number or `"auto"`. @@ -649,7 +650,7 @@ declare module 'vscode' { tabSize?: number | string; /** - * When pressing Tab insert [n](#TextEditorOptions.tabSize) spaces. + * When pressing Tab insert {@link TextEditorOptions.tabSize n} spaces. * When getting a text editor's options, this property will always be a boolean (resolved). * When setting a text editor's options, this property is optional and it can be a boolean or `"auto"`. */ @@ -672,10 +673,10 @@ declare module 'vscode' { /** * Represents a handle to a set of decorations - * sharing the same [styling options](#DecorationRenderOptions) in a [text editor](#TextEditor). + * sharing the same {@link DecorationRenderOptions styling options} in a {@link TextEditor text editor}. * * To get an instance of a `TextEditorDecorationType` use - * [createTextEditorDecorationType](#window.createTextEditorDecorationType). + * {@link window.createTextEditorDecorationType createTextEditorDecorationType}. */ export interface TextEditorDecorationType { @@ -691,7 +692,7 @@ declare module 'vscode' { } /** - * Represents different [reveal](#TextEditor.revealRange) strategies in a text editor. + * Represents different {@link TextEditor.revealRange reveal} strategies in a text editor. */ export enum TextEditorRevealType { /** @@ -714,7 +715,7 @@ declare module 'vscode' { } /** - * Represents different positions for rendering a decoration in an [overview ruler](#DecorationRenderOptions.overviewRulerLane). + * Represents different positions for rendering a decoration in an {@link DecorationRenderOptions.overviewRulerLane overview ruler}. * The overview ruler supports three lanes. */ export enum OverviewRulerLane { @@ -747,31 +748,31 @@ declare module 'vscode' { } /** - * Represents options to configure the behavior of showing a [document](#TextDocument) in an [editor](#TextEditor). + * Represents options to configure the behavior of showing a {@link TextDocument document} in an {@link TextEditor editor}. */ export interface TextDocumentShowOptions { /** - * An optional view column in which the [editor](#TextEditor) should be shown. - * The default is the [active](#ViewColumn.Active), other values are adjusted to - * be `Min(column, columnCount + 1)`, the [active](#ViewColumn.Active)-column is - * not adjusted. Use [`ViewColumn.Beside`](#ViewColumn.Beside) to open the + * An optional view column in which the {@link TextEditor editor} should be shown. + * The default is the {@link ViewColumn.Active active}, other values are adjusted to + * be `Min(column, columnCount + 1)`, the {@link ViewColumn.Active active}-column is + * not adjusted. Use {@link ViewColumn.Beside `ViewColumn.Beside`} to open the * editor to the side of the currently active one. */ viewColumn?: ViewColumn; /** - * An optional flag that when `true` will stop the [editor](#TextEditor) from taking focus. + * An optional flag that when `true` will stop the {@link TextEditor editor} from taking focus. */ preserveFocus?: boolean; /** - * An optional flag that controls if an [editor](#TextEditor)-tab will be replaced + * An optional flag that controls if an {@link TextEditor editor}-tab will be replaced * with the next editor or if it will be kept. */ preview?: boolean; /** - * An optional selection to apply for the document in the [editor](#TextEditor). + * An optional selection to apply for the document in the {@link TextEditor editor}. */ selection?: Range; } @@ -790,7 +791,7 @@ declare module 'vscode' { } /** - * A reference to a named icon. Currently, [File](#ThemeIcon.File), [Folder](#ThemeIcon.Folder), + * A reference to a named icon. Currently, {@link ThemeIcon.File File}, {@link ThemeIcon.Folder Folder}, * and [ThemeIcon ids](https://code.visualstudio.com/api/references/icons-in-labels#icon-listing) are supported. * Using a theme icon is preferred over a custom icon as it gives product theme authors the possibility to change the icons. * @@ -814,25 +815,25 @@ declare module 'vscode' { readonly id: string; /** - * The optional ThemeColor of the icon. The color is currently only used in [TreeItem](#TreeItem). + * The optional ThemeColor of the icon. The color is currently only used in {@link TreeItem}. */ readonly color?: ThemeColor; /** * Creates a reference to a theme icon. * @param id id of the icon. The available icons are listed in https://code.visualstudio.com/api/references/icons-in-labels#icon-listing. - * @param color optional `ThemeColor` for the icon. The color is currently only used in [TreeItem](#TreeItem). + * @param color optional `ThemeColor` for the icon. The color is currently only used in {@link TreeItem}. */ constructor(id: string, color?: ThemeColor); } /** - * Represents theme specific rendering styles for a [text editor decoration](#TextEditorDecorationType). + * Represents theme specific rendering styles for a {@link TextEditorDecorationType text editor decoration}. */ export interface ThemableDecorationRenderOptions { /** * Background color of the decoration. Use rgba() and define transparent background colors to play well with other decorations. - * Alternatively a color from the color registry can be [referenced](#ThemeColor). + * Alternatively a color from the color registry can be {@link ThemeColor referenced}. */ backgroundColor?: string | ThemeColor; @@ -1010,7 +1011,7 @@ declare module 'vscode' { } /** - * Represents rendering styles for a [text editor decoration](#TextEditorDecorationType). + * Represents rendering styles for a {@link TextEditorDecorationType text editor decoration}. */ export interface DecorationRenderOptions extends ThemableDecorationRenderOptions { /** @@ -1042,7 +1043,7 @@ declare module 'vscode' { } /** - * Represents options for a specific decoration in a [decoration set](#TextEditorDecorationType). + * Represents options for a specific decoration in a {@link TextEditorDecorationType decoration set}. */ export interface DecorationOptions { @@ -1088,7 +1089,7 @@ declare module 'vscode' { } /** - * Represents an editor that is attached to a [document](#TextDocument). + * Represents an editor that is attached to a {@link TextDocument document}. */ export interface TextEditor { @@ -1128,18 +1129,18 @@ declare module 'vscode' { /** * Perform an edit on the document associated with this text editor. * - * The given callback-function is invoked with an [edit-builder](#TextEditorEdit) which must + * The given callback-function is invoked with an {@link TextEditorEdit edit-builder} 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](#TextEditorEdit). + * @param callback A function which can create edits using an {@link TextEditorEdit edit-builder}. * @param options The undo/redo behavior around this edit. By default, undo stops will be created before and after this edit. * @return A promise that resolves with a value indicating if the edits could be applied. */ edit(callback: (editBuilder: TextEditorEdit) => void, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable; /** - * Insert a [snippet](#SnippetString) and put the editor into snippet mode. "Snippet mode" + * Insert a {@link SnippetString snippet} and put the editor into snippet mode. "Snippet mode" * means the editor adds placeholders and additional cursors so that the user can complete * or accept the snippet. * @@ -1149,18 +1150,20 @@ declare module 'vscode' { * @return A promise that resolves with a value indicating if the snippet could be inserted. Note that the promise does not signal * that the snippet is completely filled-in or accepted. */ - insertSnippet(snippet: SnippetString, location?: Position | Range | ReadonlyArray | ReadonlyArray, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable; + insertSnippet(snippet: SnippetString, location?: Position | Range | readonly Position[] | readonly Range[], options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable; /** * Adds a set of decorations to the text editor. If a set of decorations already exists with - * the given [decoration type](#TextEditorDecorationType), they will be replaced. + * the given {@link TextEditorDecorationType decoration type}, they will be replaced. If + * `rangesOrOptions` is empty, the existing decorations with the given {@link TextEditorDecorationType decoration type} + * will be removed. * - * @see [createTextEditorDecorationType](#window.createTextEditorDecorationType). + * @see {@link window.createTextEditorDecorationType createTextEditorDecorationType}. * * @param decorationType A decoration type. - * @param rangesOrOptions Either [ranges](#Range) or more detailed [options](#DecorationOptions). + * @param rangesOrOptions Either {@link Range ranges} or more detailed {@link DecorationOptions options}. */ - setDecorations(decorationType: TextEditorDecorationType, rangesOrOptions: Range[] | DecorationOptions[]): void; + setDecorations(decorationType: TextEditorDecorationType, rangesOrOptions: readonly Range[] | readonly DecorationOptions[]): void; /** * Scroll as indicated by `revealType` in order to reveal the given range. @@ -1173,9 +1176,9 @@ declare module 'vscode' { /** * Show the text editor. * - * @deprecated Use [window.showTextDocument](#window.showTextDocument) instead. + * @deprecated Use {@link window.showTextDocument} instead. * - * @param column The [column](#ViewColumn) in which to show this editor. + * @param column The {@link ViewColumn column} in which to show this editor. * This method shows unexpected behavior and will be removed in the next major update. */ show(column?: ViewColumn): void; @@ -1190,7 +1193,7 @@ declare module 'vscode' { } /** - * Represents an end of line character sequence in a [document](#TextDocument). + * Represents an end of line character sequence in a {@link TextDocument document}. */ export enum EndOfLine { /** @@ -1206,12 +1209,12 @@ declare module 'vscode' { /** * A complex edit that will be applied in one transaction on a TextEditor. * This holds a description of the edits and if the edits are valid (i.e. no overlapping regions, document was not changed in the meantime, etc.) - * they can be applied on a [document](#TextDocument) associated with a [text editor](#TextEditor). + * they can be applied on a {@link TextDocument document} associated with a {@link TextEditor text editor}. */ export interface TextEditorEdit { /** * Replace a certain text region with a new value. - * You can use \r\n or \n in `value` and they will be normalized to the current [document](#TextDocument). + * You can use \r\n or \n in `value` and they will be normalized to the current {@link TextDocument document}. * * @param location The range this operation should remove. * @param value The new text this operation should insert after removing `location`. @@ -1220,8 +1223,8 @@ declare module 'vscode' { /** * Insert text at a location. - * You can use \r\n or \n in `value` and they will be normalized to the current [document](#TextDocument). - * Although the equivalent text edit can be made with [replace](#TextEditorEdit.replace), `insert` will produce a different resulting selection (it will get moved). + * You can use \r\n or \n in `value` and they will be normalized to the current {@link TextDocument document}. + * Although the equivalent text edit can be made with {@link TextEditorEdit.replace replace}, `insert` will produce a different resulting selection (it will get moved). * * @param location The position where the new text should be inserted. * @param value The new text this operation should insert. @@ -1238,7 +1241,7 @@ declare module 'vscode' { /** * Set the end of line sequence. * - * @param endOfLine The new end of line for the [document](#TextDocument). + * @param endOfLine The new end of line for the {@link TextDocument document}. */ setEndOfLine(endOfLine: EndOfLine): void; } @@ -1257,7 +1260,7 @@ declare module 'vscode' { * as all uris should have a scheme. To avoid breakage of existing code the optional * `strict`-argument has been added. We *strongly* advise to use it, e.g. `Uri.parse('my:uri', true)` * - * @see [Uri.toString](#Uri.toString) + * @see {@link Uri.toString} * @param value The string value of an Uri. * @param strict Throw an error when `value` is empty or when no `scheme` can be parsed. * @return A new Uri instance. @@ -1265,10 +1268,10 @@ declare module 'vscode' { static parse(value: string, strict?: boolean): Uri; /** - * Create an URI from a file system path. The [scheme](#Uri.scheme) + * Create an URI from a file system path. The {@link Uri.scheme scheme} * will be `file`. * - * The *difference* between `Uri#parse` and `Uri#file` is that the latter treats the argument + * The *difference* between {@link Uri.parse} and {@link Uri.file} is that the latter treats the argument * as path, not as stringified-uri. E.g. `Uri.file(path)` is *not* the same as * `Uri.parse('file://' + path)` because the path might contain characters that are * interpreted (# and ?). See the following sample: @@ -1311,6 +1314,15 @@ declare module 'vscode' { */ static joinPath(base: Uri, ...pathSegments: string[]): Uri; + /** + * Create an URI from its component parts + * + * @see {@link Uri.toString} + * @param components The component parts of an Uri. + * @return A new Uri instance. + */ + static from(components: { scheme: string; authority?: string; path?: string; query?: string; fragment?: string }): Uri; + /** * Use the `file` and `parse` factory functions to create new `Uri` objects. */ @@ -1354,7 +1366,7 @@ declare module 'vscode' { * * The resulting string shall *not* be used for display purposes but * for disk operations, like `readFile` et al. * - * The *difference* to the [`path`](#Uri.path)-property is the use of the platform specific + * The *difference* to the {@link Uri.path `path`}-property is the use of the platform specific * path separator and the handling of UNC paths. The sample below outlines the difference: * ```ts const u = URI.parse('file://server/c$/folder/file.txt') @@ -1385,7 +1397,7 @@ declare module 'vscode' { * Returns a string representation of this Uri. The representation and normalization * of a URI depends on the scheme. * - * * The resulting string can be safely used with [Uri.parse](#Uri.parse). + * * The resulting string can be safely used with {@link Uri.parse}. * * The resulting string shall *not* be used for display purposes. * * *Note* that the implementation will encode _aggressive_ which often leads to unexpected, @@ -1414,7 +1426,7 @@ declare module 'vscode' { * for completion items because the user continued to type. * * To get an instance of a `CancellationToken` use a - * [CancellationTokenSource](#CancellationTokenSource). + * {@link CancellationTokenSource}. */ export interface CancellationToken { @@ -1424,13 +1436,13 @@ declare module 'vscode' { isCancellationRequested: boolean; /** - * An [event](#Event) which fires upon cancellation. + * An {@link Event} which fires upon cancellation. */ onCancellationRequested: Event; } /** - * A cancellation source creates and controls a [cancellation token](#CancellationToken). + * A cancellation source creates and controls a {@link CancellationToken cancellation token}. */ export class CancellationTokenSource { @@ -1453,7 +1465,7 @@ declare module 'vscode' { /** * 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) + * This type can be used in response to a {@link CancellationToken cancellation token} * being cancelled or when an operation is being cancelled by the * executor of that operation. */ @@ -1512,18 +1524,18 @@ declare module 'vscode' { * * @param listener The listener function will be called when the event happens. * @param thisArgs The `this`-argument which will be used when calling the event listener. - * @param disposables An array to which a [disposable](#Disposable) will be added. + * @param disposables An array to which a {@link Disposable} will be added. * @return A disposable which unsubscribes the event listener. */ (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]): Disposable; } /** - * An event emitter can be used to create and manage an [event](#Event) for others + * An event emitter can be used to create and manage an {@link Event} for others * to subscribe to. One emitter always owns one event. * * Use this class if you want to provide event from within your extension, for instance - * inside a [TextDocumentContentProvider](#TextDocumentContentProvider) or when providing + * inside a {@link TextDocumentContentProvider} or when providing * API to other extensions. */ export class EventEmitter { @@ -1534,7 +1546,7 @@ declare module 'vscode' { event: Event; /** - * Notify all subscribers of the [event](#EventEmitter.event). Failure + * Notify all subscribers of the {@link EventEmitter.event event}. Failure * of one or more listener will not fail this function call. * * @param data The event object. @@ -1549,10 +1561,10 @@ declare module 'vscode' { /** * A file system watcher notifies about changes to files and folders - * on disk or from other [FileSystemProviders](#FileSystemProvider). + * on disk or from other {@link FileSystemProvider FileSystemProviders}. * * To get an instance of a `FileSystemWatcher` use - * [createFileSystemWatcher](#workspace.createFileSystemWatcher). + * {@link workspace.createFileSystemWatcher createFileSystemWatcher}. */ export interface FileSystemWatcher extends Disposable { @@ -1594,9 +1606,9 @@ declare module 'vscode' { * A text document content provider allows to add readonly documents * to the editor, such as source from a dll or generated html from md. * - * Content providers are [registered](#workspace.registerTextDocumentContentProvider) - * for a [uri-scheme](#Uri.scheme). When a uri with that scheme is to - * be [loaded](#workspace.openTextDocument) the content provider is + * Content providers are {@link workspace.registerTextDocumentContentProvider registered} + * for a {@link Uri.scheme uri-scheme}. When a uri with that scheme is to + * be {@link workspace.openTextDocument loaded} the content provider is * asked. */ export interface TextDocumentContentProvider { @@ -1610,13 +1622,13 @@ declare module 'vscode' { * Provide textual content for a given uri. * * The editor will use the returned string-content to create a readonly - * [document](#TextDocument). Resources allocated should be released when - * the corresponding document has been [closed](#workspace.onDidCloseTextDocument). + * {@link TextDocument document}. Resources allocated should be released when + * the corresponding document has been {@link workspace.onDidCloseTextDocument closed}. * - * **Note**: The contents of the created [document](#TextDocument) might not be + * **Note**: The contents of the created {@link TextDocument document} might not be * identical to the provided text due to end-of-line-sequence normalization. * - * @param uri An uri which scheme matches the scheme this provider was [registered](#workspace.registerTextDocumentContentProvider) for. + * @param uri An uri which scheme matches the scheme this provider was {@link workspace.registerTextDocumentContentProvider registered} for. * @param token A cancellation token. * @return A string or a thenable that resolves to such. */ @@ -1630,20 +1642,20 @@ declare module 'vscode' { export interface QuickPickItem { /** - * A human-readable string which is rendered prominent. Supports rendering of [theme icons](#ThemeIcon) via + * A human-readable string which is rendered prominent. Supports rendering of {@link ThemeIcon theme icons} via * the `$()`-syntax. */ label: string; /** * A human-readable string which is rendered less prominent in the same line. Supports rendering of - * [theme icons](#ThemeIcon) via the `$()`-syntax. + * {@link ThemeIcon theme icons} via the `$()`-syntax. */ description?: string; /** * A human-readable string which is rendered less prominent in a separate line. Supports rendering of - * [theme icons](#ThemeIcon) via the `$()`-syntax. + * {@link ThemeIcon theme icons} via the `$()`-syntax. */ detail?: string; @@ -1651,7 +1663,7 @@ declare module 'vscode' { * Optional flag indicating if this item is picked initially. * (Only honored when the picker allows multiple selections.) * - * @see [QuickPickOptions.canPickMany](#QuickPickOptions.canPickMany) + * @see {@link QuickPickOptions.canPickMany} */ picked?: boolean; @@ -1703,7 +1715,7 @@ declare module 'vscode' { } /** - * Options to configure the behaviour of the [workspace folder](#WorkspaceFolder) pick UI. + * Options to configure the behaviour of the {@link WorkspaceFolder workspace folder} pick UI. */ export interface WorkspaceFolderPickOptions { @@ -1812,9 +1824,9 @@ declare module 'vscode' { * Represents an action that is shown with an information, warning, or * error message. * - * @see [showInformationMessage](#window.showInformationMessage) - * @see [showWarningMessage](#window.showWarningMessage) - * @see [showErrorMessage](#window.showErrorMessage) + * @see {@link window.showInformationMessage showInformationMessage} + * @see {@link window.showWarningMessage showWarningMessage} + * @see {@link window.showErrorMessage showErrorMessage} */ export interface MessageItem { @@ -1836,9 +1848,9 @@ declare module 'vscode' { /** * Options to configure the behavior of the message. * - * @see [showInformationMessage](#window.showInformationMessage) - * @see [showWarningMessage](#window.showWarningMessage) - * @see [showErrorMessage](#window.showErrorMessage) + * @see {@link window.showInformationMessage showInformationMessage} + * @see {@link window.showWarningMessage showWarningMessage} + * @see {@link window.showErrorMessage showErrorMessage} */ export interface MessageOptions { @@ -1864,7 +1876,7 @@ declare module 'vscode' { value?: string; /** - * Selection of the prefilled [`value`](#InputBoxOptions.value). Defined as tuple of two number where the + * Selection of the prefilled {@link InputBoxOptions.value `value`}. Defined as tuple of two number where the * first is the inclusive start index and the second the exclusive end index. When `undefined` the whole * word will be selected, when empty (start equals end) only the cursor will be set, * otherwise the defined range will be selected. @@ -1905,7 +1917,7 @@ declare module 'vscode' { /** * A relative pattern is a helper to construct glob patterns that are matched * relatively to a base file path. The base path can either be an absolute file - * path as string or uri or a [workspace folder](#WorkspaceFolder), which is the + * path as string or uri or a {@link WorkspaceFolder workspace folder}, which is the * preferred way of creating the relative pattern. */ export class RelativePattern { @@ -1942,7 +1954,7 @@ declare module 'vscode' { * ``` * * @param base A base to which this pattern will be matched against relatively. It is recommended - * to pass in a [workspace folder](#WorkspaceFolder) if the pattern should match inside the workspace. + * to pass in a {@link WorkspaceFolder workspace folder} if the pattern should match inside the workspace. * Otherwise, a uri or string should only be used if the pattern is for a file path outside the workspace. * @param pattern A file glob pattern like `*.{ts,js}` that will be matched on paths relative to the base. */ @@ -1951,7 +1963,7 @@ declare module 'vscode' { /** * A file glob pattern to match file paths against. This can either be a glob pattern string - * (like `**​/*.{ts,js}` or `*.{ts,js}`) or a [relative pattern](#RelativePattern). + * (like `**​/*.{ts,js}` or `*.{ts,js}`) or a {@link RelativePattern relative pattern}. * * Glob patterns can have the following syntax: * * `*` to match one or more characters in a path segment @@ -1962,7 +1974,7 @@ declare module 'vscode' { * * `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) * * Note: a backslash (`\`) is not valid within a glob pattern. If you have an existing file - * path to match against, consider to use the [relative pattern](#RelativePattern) support + * path to match against, consider to use the {@link RelativePattern relative pattern} support * that takes care of converting any backslash into slash. Otherwise, make sure to convert * any backslash to slash when creating the glob pattern. */ @@ -1970,14 +1982,14 @@ declare module 'vscode' { /** * A document filter denotes a document by different properties like - * the [language](#TextDocument.languageId), the [scheme](#Uri.scheme) of - * its resource, or a glob-pattern that is applied to the [path](#TextDocument.fileName). + * the {@link TextDocument.languageId language}, the {@link Uri.scheme scheme} of + * its resource, or a glob-pattern that is applied to the {@link TextDocument.fileName path}. * * @example A language filter that applies to typescript files on disk * { language: 'typescript', scheme: 'file' } * * @example A language filter that applies to all package.json paths - * { language: 'json', scheme: 'untitled', pattern: '**​/package.json' } + * { language: 'json', pattern: '**​/package.json' } */ export interface DocumentFilter { @@ -1987,20 +1999,20 @@ declare module 'vscode' { readonly language?: string; /** - * A Uri [scheme](#Uri.scheme), like `file` or `untitled`. + * A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. */ readonly scheme?: string; /** - * A [glob pattern](#GlobPattern) that is matched on the absolute path of the document. Use a [relative pattern](#RelativePattern) - * to filter documents to a [workspace folder](#WorkspaceFolder). + * A {@link GlobPattern glob pattern} that is matched on the absolute path of the document. Use a {@link RelativePattern relative pattern} + * to filter documents to a {@link WorkspaceFolder workspace folder}. */ readonly pattern?: GlobPattern; } /** * A language selector is the combination of one or many language identifiers - * and [language filters](#DocumentFilter). + * and {@link DocumentFilter language filters}. * * *Note* that a document selector that is just a language identifier selects *all* * documents, even those that are not saved on disk. Only use such selectors when @@ -2013,12 +2025,12 @@ declare module 'vscode' { export type DocumentSelector = DocumentFilter | string | ReadonlyArray; /** - * A provider result represents the values a provider, like the [`HoverProvider`](#HoverProvider), + * A provider result represents the values a provider, like the {@link HoverProvider `HoverProvider`}, * may return. For once this is the actual result type `T`, like `Hover`, or a thenable that resolves * to that type `T`. In addition, `null` and `undefined` can be returned - either directly or from a * thenable. * - * The snippets below are all valid implementations of the [`HoverProvider`](#HoverProvider): + * The snippets below are all valid implementations of the {@link HoverProvider `HoverProvider`}: * * ```ts * let a: HoverProvider = { @@ -2049,7 +2061,7 @@ declare module 'vscode' { * * Kinds are a hierarchical list of identifiers separated by `.`, e.g. `"refactor.extract.function"`. * - * Code action kinds are used by VS Code for UI elements such as the refactoring context menu. Users + * Code action kinds are used by the editor for UI elements such as the refactoring context menu. Users * can also trigger code actions with a specific kind with the `editor.action.codeAction` command. */ export class CodeActionKind { @@ -2188,7 +2200,7 @@ declare module 'vscode' { /** * Contains additional diagnostic information about the context in which - * a [code action](#CodeActionProvider.provideCodeActions) is run. + * a {@link CodeActionProvider.provideCodeActions code action} is run. */ export interface CodeActionContext { /** @@ -2199,7 +2211,7 @@ declare module 'vscode' { /** * An array of diagnostics. */ - readonly diagnostics: ReadonlyArray; + readonly diagnostics: readonly Diagnostic[]; /** * Requested kind of actions to return. @@ -2213,7 +2225,7 @@ declare module 'vscode' { * A code action represents a change that can be performed in code, e.g. to fix a problem or * to refactor code. * - * A CodeAction must set either [`edit`](#CodeAction.edit) and/or a [`command`](#CodeAction.command). If both are supplied, the `edit` is applied first, then the command is executed. + * A CodeAction must set either {@link CodeAction.edit `edit`} and/or a {@link CodeAction.command `command`}. If both are supplied, the `edit` is applied first, then the command is executed. */ export class CodeAction { @@ -2223,25 +2235,25 @@ declare module 'vscode' { title: string; /** - * A [workspace edit](#WorkspaceEdit) this code action performs. + * A {@link WorkspaceEdit workspace edit} this code action performs. */ edit?: WorkspaceEdit; /** - * [Diagnostics](#Diagnostic) that this code action resolves. + * {@link Diagnostic Diagnostics} that this code action resolves. */ diagnostics?: Diagnostic[]; /** - * A [command](#Command) this code action executes. + * A {@link Command} this code action executes. * - * If this command throws an exception, VS Code displays the exception message to users in the editor at the + * If this command throws an exception, the editor displays the exception message to users in the editor at the * current cursor position. */ command?: Command; /** - * [Kind](#CodeActionKind) of the code action. + * {@link CodeActionKind Kind} of the code action. * * Used to filter code actions. */ @@ -2266,7 +2278,7 @@ declare module 'vscode' { * of code action, such as refactorings. * * - If the user has a [keybinding](https://code.visualstudio.com/docs/editor/refactoring#_keybindings-for-code-actions) - * that auto applies a code action and only a disabled code actions are returned, VS Code will show the user an + * that auto applies a code action and only a disabled code actions are returned, the editor will show the user an * error message with `reason` in the editor. */ disabled?: { @@ -2281,8 +2293,8 @@ declare module 'vscode' { /** * Creates a new code action. * - * A code action must have at least a [title](#CodeAction.title) and [edits](#CodeAction.edit) - * and/or a [command](#CodeAction.command). + * A code action must have at least a {@link CodeAction.title title} and {@link CodeAction.edit edits} + * and/or a {@link CodeAction.command command}. * * @param title The title of the code action. * @param kind The kind of the code action. @@ -2294,7 +2306,7 @@ declare module 'vscode' { * The code action interface defines the contract between extensions and * the [lightbulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) feature. * - * A code action can be any command that is [known](#commands.getCommands) to the system. + * A code action can be any command that is {@link commands.getCommands known} to the system. */ export interface CodeActionProvider { /** @@ -2315,7 +2327,7 @@ declare module 'vscode' { provideCodeActions(document: TextDocument, range: Range | Selection, context: CodeActionContext, token: CancellationToken): ProviderResult<(Command | T)[]>; /** - * Given a code action fill in its [`edit`](#CodeAction.edit)-property. Changes to + * Given a code action fill in its {@link CodeAction.edit `edit`}-property. Changes to * all other properties, like title, are ignored. A code action that has an edit * will not be resolved. * @@ -2332,28 +2344,28 @@ declare module 'vscode' { } /** - * Metadata about the type of code actions that a [CodeActionProvider](#CodeActionProvider) provides. + * Metadata about the type of code actions that a {@link CodeActionProvider} provides. */ export interface CodeActionProviderMetadata { /** - * List of [CodeActionKinds](#CodeActionKind) that a [CodeActionProvider](#CodeActionProvider) may return. + * List of {@link CodeActionKind CodeActionKinds} that a {@link CodeActionProvider} may return. * * This list is used to determine if a given `CodeActionProvider` should be invoked or not. * To avoid unnecessary computation, every `CodeActionProvider` should list use `providedCodeActionKinds`. The * list of kinds may either be generic, such as `[CodeActionKind.Refactor]`, or list out every kind provided, * such as `[CodeActionKind.Refactor.Extract.append('function'), CodeActionKind.Refactor.Extract.append('constant'), ...]`. */ - readonly providedCodeActionKinds?: ReadonlyArray; + readonly providedCodeActionKinds?: readonly CodeActionKind[]; /** * Static documentation for a class of code actions. * * Documentation from the provider is shown in the code actions menu if either: * - * - Code actions of `kind` are requested by VS Code. In this case, VS Code will show the documentation that + * - Code actions of `kind` are requested by the editor. In this case, the editor will show the documentation that * most closely matches the requested code action kind. For example, if a provider has documentation for * both `Refactor` and `RefactorExtract`, when the user requests code actions for `RefactorExtract`, - * VS Code will use the documentation for `RefactorExtract` instead of the documentation for `Refactor`. + * the editor will use the documentation for `RefactorExtract` instead of the documentation for `Refactor`. * * - Any code actions of `kind` are returned by the provider. * @@ -2372,23 +2384,23 @@ declare module 'vscode' { /** * Command that displays the documentation to the user. * - * This can display the documentation directly in VS Code or open a website using [`env.openExternal`](#env.openExternal); + * This can display the documentation directly in the editor or open a website using {@link env.openExternal `env.openExternal`}; * - * The title of this documentation code action is taken from [`Command.title`](#Command.title) + * The title of this documentation code action is taken from {@link Command.title `Command.title`} */ readonly command: Command; }>; } /** - * A code lens represents a [command](#Command) that should be shown along with + * A code lens represents a {@link Command} that should be shown along with * source text, like the number of references, a way to run tests, etc. * * A code lens is _unresolved_ when no command is associated to it. For performance * reasons the creation of a code lens and resolving should be done to two stages. * - * @see [CodeLensProvider.provideCodeLenses](#CodeLensProvider.provideCodeLenses) - * @see [CodeLensProvider.resolveCodeLens](#CodeLensProvider.resolveCodeLens) + * @see {@link CodeLensProvider.provideCodeLenses} + * @see {@link CodeLensProvider.resolveCodeLens} */ export class CodeLens { @@ -2417,7 +2429,7 @@ declare module 'vscode' { } /** - * A code lens provider adds [commands](#Command) to source text. The commands will be shown + * A code lens provider adds {@link Command commands} to source text. The commands will be shown * as dedicated horizontal lines in between the source text. */ export interface CodeLensProvider { @@ -2428,9 +2440,9 @@ declare module 'vscode' { onDidChangeCodeLenses?: Event; /** - * Compute a list of [lenses](#CodeLens). This call should return as fast as possible and if + * Compute a list of {@link CodeLens lenses}. This call should return as fast as possible and if * computing the commands is expensive implementors should only return code lens objects with the - * range set and implement [resolve](#CodeLensProvider.resolveCodeLens). + * range set and implement {@link CodeLensProvider.resolveCodeLens resolve}. * * @param document The document in which the command was invoked. * @param token A cancellation token. @@ -2441,7 +2453,7 @@ declare module 'vscode' { /** * This function will be called for each visible code lens, usually when scrolling and after - * calls to [compute](#CodeLensProvider.provideCodeLenses)-lenses. + * calls to {@link CodeLensProvider.provideCodeLenses compute}-lenses. * * @param codeLens Code lens that must be resolved. * @param token A cancellation token. @@ -2453,7 +2465,7 @@ declare module 'vscode' { /** * Information about where a symbol is defined. * - * Provides additional metadata over normal {@link Location location} definitions, including the range of + * Provides additional metadata over normal {@link Location} definitions, including the range of * the defining symbol */ export type DefinitionLink = LocationLink; @@ -2521,8 +2533,8 @@ declare module 'vscode' { } /** - * The declaration of a symbol representation as one or many [locations](#Location) - * or [location links](#LocationLink). + * The declaration of a symbol representation as one or many {@link Location locations} + * or {@link LocationLink location links}. */ export type Declaration = Location | Location[] | LocationLink[]; @@ -2548,7 +2560,7 @@ declare module 'vscode' { * The MarkdownString represents human-readable text that supports formatting via the * markdown syntax. Standard markdown is supported, also tables, but no embedded html. * - * When created with `supportThemeIcons` then rendering of [theme icons](#ThemeIcon) via + * When created with `supportThemeIcons` then rendering of {@link ThemeIcon theme icons} via * the `$()`-syntax is supported. */ export class MarkdownString { @@ -2565,7 +2577,7 @@ declare module 'vscode' { isTrusted?: boolean; /** - * Indicates that this markdown string can contain [ThemeIcons](#ThemeIcon), e.g. `$(zap)`. + * Indicates that this markdown string can contain {@link ThemeIcon ThemeIcons}, e.g. `$(zap)`. */ readonly supportThemeIcons?: boolean; @@ -2573,7 +2585,7 @@ declare module 'vscode' { * Creates a new markdown string with the given value. * * @param value Optional, initial value. - * @param supportThemeIcons Optional, Specifies whether [ThemeIcons](#ThemeIcon) are supported within the [`MarkdownString`](#MarkdownString). + * @param supportThemeIcons Optional, Specifies whether {@link ThemeIcon ThemeIcons} are supported within the {@link MarkdownString `MarkdownString`}. */ constructor(value?: string, supportThemeIcons?: boolean); @@ -2584,7 +2596,7 @@ declare module 'vscode' { appendText(value: string): MarkdownString; /** - * Appends the given string 'as is' to this markdown string. When [`supportThemeIcons`](#MarkdownString.supportThemeIcons) is `true`, [ThemeIcons](#ThemeIcon) in the `value` will be iconified. + * Appends the given string 'as is' to this markdown string. When {@link MarkdownString.supportThemeIcons `supportThemeIcons`} is `true`, {@link ThemeIcon ThemeIcons} in the `value` will be iconified. * @param value Markdown string. */ appendMarkdown(value: string): MarkdownString; @@ -2592,7 +2604,7 @@ declare module 'vscode' { /** * Appends the given string as codeblock using the provided language. * @param value A code snippet. - * @param language An optional [language identifier](#languages.getLanguages). + * @param language An optional {@link languages.getLanguages language identifier}. */ appendCodeblock(value: string, language?: string): MarkdownString; } @@ -2602,7 +2614,7 @@ declare module 'vscode' { * or a code-block that provides a language and a code snippet. Note that * markdown strings will be sanitized - that means html will be escaped. * - * @deprecated This type is deprecated, please use [`MarkdownString`](#MarkdownString) instead. + * @deprecated This type is deprecated, please use {@link MarkdownString `MarkdownString`} instead. */ export type MarkedString = MarkdownString | string | { language: string; value: string }; @@ -2684,13 +2696,13 @@ declare module 'vscode' { /** * The evaluatable expression provider interface defines the contract between extensions and * the debug hover. In this contract the provider returns an evaluatable expression for a given position - * in a document and VS Code evaluates this expression in the active debug session and shows the result in a debug hover. + * in a document and the editor evaluates this expression in the active debug session and shows the result in a debug hover. */ export interface EvaluatableExpressionProvider { /** * Provide an evaluatable expression for the given document and position. - * VS Code will evaluate this expression in the active debug session and will show the result in the debug hover. + * The editor will evaluate this expression in the active debug session and will show the result in the debug hover. * The expression can be implicitly specified by the range in the underlying document or by explicitly returning an expression. * * @param document The document for which the debug hover is about to appear. @@ -2803,21 +2815,21 @@ declare module 'vscode' { } /** - * The inline values provider interface defines the contract between extensions and the VS Code debugger inline values feature. + * The inline values provider interface defines the contract between extensions and the editor's debugger inline values feature. * In this contract the provider returns inline value information for a given document range - * and VS Code shows this information in the editor at the end of lines. + * and the editor shows this information in the editor at the end of lines. */ export interface InlineValuesProvider { /** * An optional event to signal that inline values have changed. - * @see [EventEmitter](#EventEmitter) + * @see {@link EventEmitter} */ onDidChangeInlineValues?: Event | undefined; /** * Provide "inline value" information for a given document and range. - * VS Code calls this method whenever debugging stops in the given document. + * The editor calls this method whenever debugging stops in the given document. * The returned inline values information is rendered in the editor at the end of lines. * * @param document The document for which the inline values information is needed. @@ -2864,7 +2876,7 @@ declare module 'vscode' { range: Range; /** - * The highlight kind, default is [text](#DocumentHighlightKind.Text). + * The highlight kind, default is {@link DocumentHighlightKind.Text text}. */ kind?: DocumentHighlightKind; @@ -2872,7 +2884,7 @@ declare module 'vscode' { * Creates a new document highlight object. * * @param range The range the highlight applies to. - * @param kind The highlight kind, default is [text](#DocumentHighlightKind.Text). + * @param kind The highlight kind, default is {@link DocumentHighlightKind.Text text}. */ constructor(range: Range, kind?: DocumentHighlightKind); } @@ -2963,7 +2975,7 @@ declare module 'vscode' { /** * Tags for this symbol. */ - tags?: ReadonlyArray; + tags?: readonly SymbolTag[]; /** * The location of this symbol. @@ -2983,7 +2995,7 @@ declare module 'vscode' { /** * Creates a new symbol information object. * - * @deprecated Please use the constructor taking a [location](#Location) object. + * @deprecated Please use the constructor taking a {@link Location} object. * * @param name The name of the symbol. * @param kind The kind of the symbol. @@ -3019,7 +3031,7 @@ declare module 'vscode' { /** * Tags for this symbol. */ - tags?: ReadonlyArray; + tags?: readonly SymbolTag[]; /** * The range enclosing this symbol not including leading/trailing whitespace but everything else, e.g. comments and code. @@ -3028,7 +3040,7 @@ declare module 'vscode' { /** * The range that should be selected and reveal when this symbol is being picked, e.g. the name of a function. - * Must be contained by the [`range`](#DocumentSymbol.range). + * Must be contained by the {@link DocumentSymbol.range `range`}. */ selectionRange: Range; @@ -3091,7 +3103,7 @@ declare module 'vscode' { * strict matching. * * To improve performance implementors can implement `resolveWorkspaceSymbol` and then provide symbols with partial - * [location](#SymbolInformation.location)-objects, without a `range` defined. The editor will then call + * {@link SymbolInformation.location location}-objects, without a `range` defined. The editor will then call * `resolveWorkspaceSymbol` for selected symbols only, e.g. when opening a workspace symbol. * * @param query A query string, can be the empty string in which case all symbols should be returned. @@ -3102,9 +3114,9 @@ declare module 'vscode' { provideWorkspaceSymbols(query: string, token: CancellationToken): ProviderResult; /** - * Given a symbol fill in its [location](#SymbolInformation.location). This method is called whenever a symbol + * Given a symbol fill in its {@link SymbolInformation.location location}. This method is called whenever a symbol * is selected in the UI. Providers can implement this method and return incomplete symbols from - * [`provideWorkspaceSymbols`](#WorkspaceSymbolProvider.provideWorkspaceSymbols) which often helps to improve + * {@link WorkspaceSymbolProvider.provideWorkspaceSymbols `provideWorkspaceSymbols`} which often helps to improve * performance. * * @param symbol The symbol that is to be resolved. Guaranteed to be an instance of an object returned from an @@ -3237,7 +3249,7 @@ declare module 'vscode' { description?: string; /** - * The icon path or [ThemeIcon](#ThemeIcon) for the edit. + * The icon path or {@link ThemeIcon} for the edit. */ iconPath?: Uri | { light: Uri; dark: Uri } | ThemeIcon; } @@ -3246,7 +3258,7 @@ declare module 'vscode' { * A workspace edit is a collection of textual and files changes for * multiple resources and documents. * - * Use the [applyEdit](#workspace.applyEdit)-function to apply a workspace edit. + * Use the {@link workspace.applyEdit applyEdit}-function to apply a workspace edit. */ export class WorkspaceEdit { @@ -3368,7 +3380,7 @@ declare module 'vscode' { /** * Builder-function that appends the given string to - * the [`value`](#SnippetString.value) of this snippet string. + * the {@link SnippetString.value `value`} of this snippet string. * * @param string A value to append 'as given'. The string will be escaped. * @return This snippet string. @@ -3377,7 +3389,7 @@ declare module 'vscode' { /** * Builder-function that appends a tabstop (`$1`, `$2` etc) to - * the [`value`](#SnippetString.value) of this snippet string. + * the {@link SnippetString.value `value`} of this snippet string. * * @param number The number of this tabstop, defaults to an auto-increment * value starting at 1. @@ -3387,7 +3399,7 @@ declare module 'vscode' { /** * Builder-function that appends a placeholder (`${1:value}`) to - * the [`value`](#SnippetString.value) of this snippet string. + * the {@link SnippetString.value `value`} of this snippet string. * * @param value The value of this placeholder - either a string or a function * with which a nested snippet can be created. @@ -3399,7 +3411,7 @@ declare module 'vscode' { /** * Builder-function that appends a choice (`${1|a,b,c|}`) to - * the [`value`](#SnippetString.value) of this snippet string. + * the {@link SnippetString.value `value`} of this snippet string. * * @param values The values for choices - the array of strings * @param number The number of this tabstop, defaults to an auto-increment @@ -3410,7 +3422,7 @@ declare module 'vscode' { /** * Builder-function that appends a variable (`${VAR}`) to - * the [`value`](#SnippetString.value) of this snippet string. + * the {@link SnippetString.value `value`} of this snippet string. * * @param name The name of the variable - excluding the `$`. * @param defaultValue The default value which is used when the variable name cannot @@ -3508,8 +3520,8 @@ declare module 'vscode' { /** * Represents semantic tokens, either in a range or in an entire document. - * @see [provideDocumentSemanticTokens](#DocumentSemanticTokensProvider.provideDocumentSemanticTokens) for an explanation of the format. - * @see [SemanticTokensBuilder](#SemanticTokensBuilder) for a helper to create an instance. + * @see {@link DocumentSemanticTokensProvider.provideDocumentSemanticTokens provideDocumentSemanticTokens} for an explanation of the format. + * @see {@link SemanticTokensBuilder} for a helper to create an instance. */ export class SemanticTokens { /** @@ -3520,7 +3532,7 @@ declare module 'vscode' { readonly resultId?: string; /** * The actual tokens data. - * @see [provideDocumentSemanticTokens](#DocumentSemanticTokensProvider.provideDocumentSemanticTokens) for an explanation of the format. + * @see {@link DocumentSemanticTokensProvider.provideDocumentSemanticTokens provideDocumentSemanticTokens} for an explanation of the format. */ readonly data: Uint32Array; @@ -3529,7 +3541,7 @@ declare module 'vscode' { /** * Represents edits to semantic tokens. - * @see [provideDocumentSemanticTokensEdits](#DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits) for an explanation of the format. + * @see {@link DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits provideDocumentSemanticTokensEdits} for an explanation of the format. */ export class SemanticTokensEdits { /** @@ -3549,7 +3561,7 @@ declare module 'vscode' { /** * Represents an edit to semantic tokens. - * @see [provideDocumentSemanticTokensEdits](#DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits) for an explanation of the format. + * @see {@link DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits provideDocumentSemanticTokensEdits} for an explanation of the format. */ export class SemanticTokensEdit { /** @@ -3633,8 +3645,8 @@ declare module 'vscode' { * [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] * ``` * - * @see [SemanticTokensBuilder](#SemanticTokensBuilder) for a helper to encode tokens as integers. - * *NOTE*: When doing edits, it is possible that multiple edits occur until VS Code decides to invoke the semantic tokens provider. + * @see {@link SemanticTokensBuilder} for a helper to encode tokens as integers. + * *NOTE*: When doing edits, it is possible that multiple edits occur until the editor decides to invoke the semantic tokens provider. * *NOTE*: If the provider cannot temporarily compute semantic tokens, it can indicate this by throwing an error with the message 'Busy'. */ provideDocumentSemanticTokens(document: TextDocument, token: CancellationToken): ProviderResult; @@ -3677,7 +3689,7 @@ declare module 'vscode' { */ export interface DocumentRangeSemanticTokensProvider { /** - * @see [provideDocumentSemanticTokens](#DocumentSemanticTokensProvider.provideDocumentSemanticTokens). + * @see {@link DocumentSemanticTokensProvider.provideDocumentSemanticTokens provideDocumentSemanticTokens}. */ provideDocumentRangeSemanticTokens(document: TextDocument, range: Range, token: CancellationToken): ProviderResult; } @@ -3778,8 +3790,8 @@ declare module 'vscode' { * The label of this signature. * * Either a string or inclusive start and exclusive end offsets within its containing - * [signature label](#SignatureInformation.label). *Note*: A label of type string must be - * a substring of its containing signature information's [label](#SignatureInformation.label). + * {@link SignatureInformation.label signature label}. *Note*: A label of type string must be + * a substring of its containing signature information's {@link SignatureInformation.label label}. */ label: string | [number, number]; @@ -3825,7 +3837,7 @@ declare module 'vscode' { /** * The index of the active parameter. * - * If provided, this is used in place of [`SignatureHelp.activeSignature`](#SignatureHelp.activeSignature). + * If provided, this is used in place of {@link SignatureHelp.activeSignature `SignatureHelp.activeSignature`}. */ activeParameter?: number; @@ -3862,7 +3874,7 @@ declare module 'vscode' { } /** - * How a [`SignatureHelpProvider`](#SignatureHelpProvider) was triggered. + * How a {@link SignatureHelpProvider `SignatureHelpProvider`} was triggered. */ export enum SignatureHelpTriggerKind { /** @@ -3883,7 +3895,7 @@ declare module 'vscode' { /** * Additional information about the context in which a - * [`SignatureHelpProvider`](#SignatureHelpProvider.provideSignatureHelp) was triggered. + * {@link SignatureHelpProvider.provideSignatureHelp `SignatureHelpProvider`} was triggered. */ export interface SignatureHelpContext { /** @@ -3908,7 +3920,7 @@ declare module 'vscode' { readonly isRetrigger: boolean; /** - * The currently active [`SignatureHelp`](#SignatureHelp). + * The currently active {@link SignatureHelp `SignatureHelp`}. * * The `activeSignatureHelp` has its [`SignatureHelp.activeSignature`] field updated based on * the user arrowing through available signatures. @@ -3937,13 +3949,13 @@ declare module 'vscode' { } /** - * Metadata about a registered [`SignatureHelpProvider`](#SignatureHelpProvider). + * Metadata about a registered {@link SignatureHelpProvider `SignatureHelpProvider`}. */ export interface SignatureHelpProviderMetadata { /** * List of characters that trigger signature help. */ - readonly triggerCharacters: ReadonlyArray; + readonly triggerCharacters: readonly string[]; /** * List of characters that re-trigger signature help. @@ -3951,7 +3963,7 @@ declare module 'vscode' { * These trigger characters are only active when signature help is already showing. All trigger characters * are also counted as re-trigger characters. */ - readonly retriggerCharacters: ReadonlyArray; + readonly retriggerCharacters: readonly string[]; } /** @@ -4001,17 +4013,17 @@ declare module 'vscode' { /** * A completion item represents a text snippet that is proposed to complete text that is being typed. * - * It is sufficient to create a completion item from just a [label](#CompletionItem.label). In that - * case the completion item will replace the [word](#TextDocument.getWordRangeAtPosition) - * until the cursor with the given label or [insertText](#CompletionItem.insertText). Otherwise the - * given [edit](#CompletionItem.textEdit) is used. + * It is sufficient to create a completion item from just a {@link CompletionItem.label label}. In that + * case the completion item will replace the {@link TextDocument.getWordRangeAtPosition word} + * until the cursor with the given label or {@link CompletionItem.insertText insertText}. Otherwise the + * given {@link CompletionItem.textEdit edit} is used. * * When selecting a completion item in the editor its defined or synthesized text edit will be applied - * to *all* cursors/selections whereas [additionalTextEdits](#CompletionItem.additionalTextEdits) will be + * to *all* cursors/selections whereas {@link CompletionItem.additionalTextEdits additionalTextEdits} will be * applied as provided. * - * @see [CompletionItemProvider.provideCompletionItems](#CompletionItemProvider.provideCompletionItems) - * @see [CompletionItemProvider.resolveCompletionItem](#CompletionItemProvider.resolveCompletionItem) + * @see {@link CompletionItemProvider.provideCompletionItems} + * @see {@link CompletionItemProvider.resolveCompletionItem} */ export class CompletionItem { @@ -4031,7 +4043,7 @@ declare module 'vscode' { /** * Tags for this completion item. */ - tags?: ReadonlyArray; + tags?: readonly CompletionItemTag[]; /** * A human-readable string with additional information @@ -4046,25 +4058,25 @@ declare module 'vscode' { /** * A string that should be used when comparing this item - * with other items. When `falsy` the [label](#CompletionItem.label) + * with other items. When `falsy` the {@link CompletionItem.label label} * is used. * * 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 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 + * {@link CompletionItem.range `range`}-property and can therefore be different * for each completion. */ sortText?: string; /** * A string that should be used when filtering a set of - * completion items. When `falsy` the [label](#CompletionItem.label) + * completion items. When `falsy` the {@link CompletionItem.label label} * is used. * * Note that the filter text is matched against the leading word (prefix) which is defined - * by the [`range`](#CompletionItem.range)-property. + * by the {@link CompletionItem.range `range`}-property. */ filterText?: string; @@ -4077,7 +4089,7 @@ declare module 'vscode' { /** * A string or snippet that should be inserted in a document when selecting - * this completion. When `falsy` the [label](#CompletionItem.label) + * this completion. When `falsy` the {@link CompletionItem.label label} * is used. */ insertText?: string | SnippetString; @@ -4085,12 +4097,12 @@ declare module 'vscode' { /** * A range or a insert and replace range selecting the text that should be replaced by this completion item. * - * When omitted, the range of the [current word](#TextDocument.getWordRangeAtPosition) is used as replace-range - * and as insert-range the start of the [current word](#TextDocument.getWordRangeAtPosition) to the + * When omitted, the range of the {@link TextDocument.getWordRangeAtPosition current word} is used as replace-range + * and as insert-range the start of the {@link TextDocument.getWordRangeAtPosition current word} to the * current position is used. * - * *Note 1:* A range must be a [single line](#Range.isSingleLine) and it must - * [contain](#Range.contains) the position at which completion has been [requested](#CompletionItemProvider.provideCompletionItems). + * *Note 1:* A range must be a {@link Range.isSingleLine single line} and it must + * {@link Range.contains contain} the position at which completion has been {@link CompletionItemProvider.provideCompletionItems requested}. * *Note 2:* A insert range must be a prefix of a replace range, that means it must be contained and starting at the same position. */ range?: Range | { inserting: Range; replacing: Range; }; @@ -4103,7 +4115,7 @@ declare module 'vscode' { commitCharacters?: string[]; /** - * Keep whitespace of the [insertText](#CompletionItem.insertText) as is. By default, the editor adjusts leading + * Keep whitespace of the {@link CompletionItem.insertText insertText} as is. By default, the editor adjusts leading * whitespace of new lines so that they match the indentation of the line for which the item is accepted - setting * this to `true` will prevent that. */ @@ -4112,43 +4124,43 @@ declare module 'vscode' { /** * @deprecated Use `CompletionItem.insertText` and `CompletionItem.range` instead. * - * An [edit](#TextEdit) which is applied to a document when selecting + * An {@link TextEdit edit} which is applied to a document when selecting * this completion. When an edit is provided the value of - * [insertText](#CompletionItem.insertText) is ignored. + * {@link CompletionItem.insertText insertText} is ignored. * - * The [range](#Range) of the edit must be single-line and on the same - * line completions were [requested](#CompletionItemProvider.provideCompletionItems) at. + * The {@link Range} of the edit must be single-line and on the same + * line completions were {@link CompletionItemProvider.provideCompletionItems requested} at. */ textEdit?: TextEdit; /** - * An optional array of additional [text edits](#TextEdit) that are applied when - * selecting this completion. Edits must not overlap with the main [edit](#CompletionItem.textEdit) + * An optional array of additional {@link TextEdit text edits} that are applied when + * selecting this completion. Edits must not overlap with the main {@link CompletionItem.textEdit edit} * nor with themselves. */ additionalTextEdits?: TextEdit[]; /** - * An optional [command](#Command) that is executed *after* inserting this completion. *Note* that + * An optional {@link Command} that is executed *after* inserting this completion. *Note* that * additional modifications to the current document should be described with the - * [additionalTextEdits](#CompletionItem.additionalTextEdits)-property. + * {@link CompletionItem.additionalTextEdits additionalTextEdits}-property. */ command?: Command; /** * Creates a new completion item. * - * Completion items must have at least a [label](#CompletionItem.label) which then + * Completion items must have at least a {@link CompletionItem.label label} which then * will be used as insert text as well as for sorting and filtering. * * @param label The label of the completion. - * @param kind The [kind](#CompletionItemKind) of the completion. + * @param kind The {@link CompletionItemKind kind} of the completion. */ constructor(label: string, kind?: CompletionItemKind); } /** - * Represents a collection of [completion items](#CompletionItem) to be presented + * Represents a collection of {@link CompletionItem completion items} to be presented * in the editor. */ export class CompletionList { @@ -4174,7 +4186,7 @@ declare module 'vscode' { } /** - * How a [completion provider](#CompletionItemProvider) was triggered + * How a {@link CompletionItemProvider completion provider} was triggered */ export enum CompletionTriggerKind { /** @@ -4193,7 +4205,7 @@ declare module 'vscode' { /** * Contains additional information about the context in which - * [completion provider](#CompletionItemProvider.provideCompletionItems) is triggered. + * {@link CompletionItemProvider.provideCompletionItems completion provider} is triggered. */ export interface CompletionContext { /** @@ -4215,9 +4227,9 @@ declare module 'vscode' { * The completion item provider interface defines the contract between extensions and * [IntelliSense](https://code.visualstudio.com/docs/editor/intellisense). * - * Providers can delay the computation of the [`detail`](#CompletionItem.detail) - * and [`documentation`](#CompletionItem.documentation) properties by implementing the - * [`resolveCompletionItem`](#CompletionItemProvider.resolveCompletionItem)-function. However, properties that + * Providers can delay the computation of the {@link CompletionItem.detail `detail`} + * and {@link CompletionItem.documentation `documentation`} properties by implementing the + * {@link CompletionItemProvider.resolveCompletionItem `resolveCompletionItem`}-function. However, properties that * are needed for the initial sorting and filtering, like `sortText`, `filterText`, `insertText`, and `range`, must * not be changed during resolve. * @@ -4234,22 +4246,22 @@ declare module 'vscode' { * @param token A cancellation token. * @param context How the completion was triggered. * - * @return An array of completions, a [completion list](#CompletionList), or a thenable that resolves to either. + * @return An array of completions, a {@link CompletionList completion list}, or a thenable that resolves to either. * The lack of a result can be signaled by returning `undefined`, `null`, or an empty array. */ provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): ProviderResult>; /** - * Given a completion item fill in more data, like [doc-comment](#CompletionItem.documentation) - * or [details](#CompletionItem.detail). + * Given a completion item fill in more data, like {@link CompletionItem.documentation doc-comment} + * or {@link CompletionItem.detail details}. * * The editor will only resolve a completion item once. * * *Note* that this function is called when completion items are already showing in the UI or when an item has been * selected for insertion. Because of that, no property that changes the presentation (label, sorting, filtering etc) - * or the (primary) insert behaviour ([insertText](#CompletionItem.insertText)) can be changed. + * or the (primary) insert behaviour ({@link CompletionItem.insertText insertText}) can be changed. * - * This function may fill in [additionalTextEdits](#CompletionItem.additionalTextEdits). However, that means an item might be + * This function may fill in {@link CompletionItem.additionalTextEdits additionalTextEdits}. However, that means an item might be * inserted *before* resolving is done and in that case the editor will do a best effort to still apply those additional * text edits. * @@ -4307,15 +4319,15 @@ declare module 'vscode' { * * @param document The document in which the command was invoked. * @param token A cancellation token. - * @return An array of [document links](#DocumentLink) or a thenable that resolves to such. The lack of a result + * @return An array of {@link DocumentLink document links} or a thenable that resolves to such. The lack of a result * can be signaled by returning `undefined`, `null`, or an empty array. */ provideDocumentLinks(document: TextDocument, token: CancellationToken): ProviderResult; /** - * Given a link fill in its [target](#DocumentLink.target). This method is called when an incomplete + * Given a link fill in its {@link DocumentLink.target target}. This method is called when an incomplete * link is selected in the UI. Providers can implement this method and return incomplete links - * (without target) from the [`provideDocumentLinks`](#DocumentLinkProvider.provideDocumentLinks) method which + * (without target) from the {@link DocumentLinkProvider.provideDocumentLinks `provideDocumentLinks`} method which * often helps to improve performance. * * @param link The link that is to be resolved. @@ -4386,7 +4398,7 @@ declare module 'vscode' { } /** - * A color presentation object describes how a [`color`](#Color) should be represented as text and what + * A color presentation object describes how a {@link Color `color`} should be represented as text and what * edits are required to refer to it from source code. * * For some languages one color can have multiple presentations, e.g. css can represent the color red with @@ -4403,15 +4415,15 @@ declare module 'vscode' { label: string; /** - * An [edit](#TextEdit) which is applied to a document when selecting - * this presentation for the color. When `falsy` the [label](#ColorPresentation.label) + * An {@link TextEdit edit} which is applied to a document when selecting + * this presentation for the color. When `falsy` the {@link ColorPresentation.label label} * is used. */ textEdit?: TextEdit; /** - * An optional array of additional [text edits](#TextEdit) that are applied when - * selecting this color presentation. Edits must not overlap with the main [edit](#ColorPresentation.textEdit) nor with themselves. + * An optional array of additional {@link TextEdit text edits} that are applied when + * selecting this color presentation. Edits must not overlap with the main {@link ColorPresentation.textEdit edit} nor with themselves. */ additionalTextEdits?: TextEdit[]; @@ -4434,13 +4446,13 @@ declare module 'vscode' { * * @param document The document in which the command was invoked. * @param token A cancellation token. - * @return An array of [color information](#ColorInformation) or a thenable that resolves to such. The lack of a result + * @return An array of {@link ColorInformation color information} or a thenable that resolves to such. The lack of a result * can be signaled by returning `undefined`, `null`, or an empty array. */ provideDocumentColors(document: TextDocument, token: CancellationToken): ProviderResult; /** - * Provide [representations](#ColorPresentation) for a color. + * Provide {@link ColorPresentation representations} for a color. * * @param color The color to show and insert. * @param context A context object with additional information @@ -4470,10 +4482,10 @@ declare module 'vscode' { end: number; /** - * Describes the [Kind](#FoldingRangeKind) of the folding range such as [Comment](#FoldingRangeKind.Comment) or - * [Region](#FoldingRangeKind.Region). The kind is used to categorize folding ranges and used by commands + * Describes the {@link FoldingRangeKind Kind} of the folding range such as {@link FoldingRangeKind.Comment Comment} or + * {@link FoldingRangeKind.Region Region}. The kind is used to categorize folding ranges and used by commands * like 'Fold all comments'. See - * [FoldingRangeKind](#FoldingRangeKind) for an enumeration of all kinds. + * {@link FoldingRangeKind} for an enumeration of all kinds. * If not set, the range is originated from a syntax element. */ kind?: FoldingRangeKind; @@ -4489,7 +4501,7 @@ declare module 'vscode' { } /** - * An enumeration of specific folding range kinds. The kind is an optional field of a [FoldingRange](#FoldingRange) + * An enumeration of specific folding range kinds. The kind is an optional field of a {@link FoldingRange} * and is used to distinguish specific folding ranges such as ranges originated from comments. The kind is used by commands like * `Fold all comments` or `Fold all regions`. * If the kind is not set on the range, the range originated from a syntax element other than comments, imports or region markers. @@ -4543,7 +4555,7 @@ declare module 'vscode' { export class SelectionRange { /** - * The [range](#Range) of this selection range. + * The {@link Range} of this selection range. */ range: Range; @@ -4567,7 +4579,7 @@ declare module 'vscode' { * * Selection ranges should be computed individually and independent for each position. The editor will merge * and deduplicate ranges but providers must return hierarchies of selection ranges so that a range - * is [contained](#Range.contains) by its parent. + * is {@link Range.contains contained} by its parent. * * @param document The document in which the command was invoked. * @param positions The positions at which the command was invoked. @@ -4596,7 +4608,7 @@ declare module 'vscode' { /** * Tags for this item. */ - tags?: ReadonlyArray; + tags?: readonly SymbolTag[]; /** * More detail for this item, e.g. the signature of a function. @@ -4615,7 +4627,7 @@ declare module 'vscode' { /** * The range that should be selected and revealed when this symbol is being picked, e.g. the name of a function. - * Must be contained by the [`range`](#CallHierarchyItem.range). + * Must be contained by the {@link CallHierarchyItem.range `range`}. */ selectionRange: Range; @@ -4637,7 +4649,7 @@ declare module 'vscode' { /** * The range at which at which the calls appears. This is relative to the caller - * denoted by [`this.from`](#CallHierarchyIncomingCall.from). + * denoted by {@link CallHierarchyIncomingCall.from `this.from`}. */ fromRanges: Range[]; @@ -4662,8 +4674,8 @@ declare module 'vscode' { /** * The range at which this item is called. This is the range relative to the caller, e.g the item - * passed to [`provideCallHierarchyOutgoingCalls`](#CallHierarchyProvider.provideCallHierarchyOutgoingCalls) - * and not [`this.to`](#CallHierarchyOutgoingCall.to). + * passed to {@link CallHierarchyProvider.provideCallHierarchyOutgoingCalls `provideCallHierarchyOutgoingCalls`} + * and not {@link CallHierarchyOutgoingCall.to `this.to`}. */ fromRanges: Range[]; @@ -4970,10 +4982,10 @@ declare module 'vscode' { * - *Default Settings* * - *Global (User) Settings* * - *Workspace settings* - * - *Workspace Folder settings* - From one of the [Workspace Folders](#workspace.workspaceFolders) under which requested resource belongs to. + * - *Workspace Folder settings* - From one of the {@link workspace.workspaceFolders Workspace Folders} under which requested resource belongs to. * - *Language settings* - Settings defined under requested language. * - * The *effective* value (returned by [`get`](#WorkspaceConfiguration.get)) is computed by overriding or merging the values in the following order. + * The *effective* value (returned by {@link WorkspaceConfiguration.get `get`}) is computed by overriding or merging the values in the following order. * * ``` * `defaultValue` (if defined in `package.json` otherwise derived from the value's type) @@ -5059,7 +5071,7 @@ declare module 'vscode' { * Retrieve all information about a configuration setting. A configuration value * often consists of a *default* value, a global or installation-wide value, * a workspace-specific value, folder-specific value - * and language-specific values (if [WorkspaceConfiguration](#WorkspaceConfiguration) is scoped to a language). + * and language-specific values (if {@link WorkspaceConfiguration} is scoped to a language). * * Also provides all language ids under which the given configuration setting is defined. * @@ -5091,20 +5103,20 @@ declare module 'vscode' { * * A value can be changed in * - * - [Global settings](#ConfigurationTarget.Global): Changes the value for all instances of the editor. - * - [Workspace settings](#ConfigurationTarget.Workspace): Changes the value for current workspace, if available. - * - [Workspace folder settings](#ConfigurationTarget.WorkspaceFolder): Changes the value for settings from one of the [Workspace Folders](#workspace.workspaceFolders) under which the requested resource belongs to. + * - {@link ConfigurationTarget.Global Global settings}: Changes the value for all instances of the editor. + * - {@link ConfigurationTarget.Workspace Workspace settings}: Changes the value for current workspace, if available. + * - {@link ConfigurationTarget.WorkspaceFolder Workspace folder settings}: Changes the value for settings from one of the {@link workspace.workspaceFolders Workspace Folders} under which the requested resource belongs to. * - Language settings: Changes the value for the requested languageId. * * *Note:* To remove a configuration value use `undefined`, like so: `config.update('somekey', undefined)` * * @param section Configuration name, supports _dotted_ names. * @param value The new value. - * @param configurationTarget The [configuration target](#ConfigurationTarget) or a boolean value. - * - If `true` updates [Global settings](#ConfigurationTarget.Global). - * - If `false` updates [Workspace settings](#ConfigurationTarget.Workspace). - * - If `undefined` or `null` updates to [Workspace folder settings](#ConfigurationTarget.WorkspaceFolder) if configuration is resource specific, - * otherwise to [Workspace settings](#ConfigurationTarget.Workspace). + * @param configurationTarget The {@link ConfigurationTarget configuration target} or a boolean value. + * - If `true` updates {@link ConfigurationTarget.Global Global settings}. + * - If `false` updates {@link ConfigurationTarget.Workspace Workspace settings}. + * - If `undefined` or `null` updates to {@link ConfigurationTarget.WorkspaceFolder Workspace folder settings} if configuration is resource specific, + * otherwise to {@link ConfigurationTarget.Workspace Workspace settings}. * @param overrideInLanguage Whether to update the value in the scope of requested languageId or not. * - If `true` updates the value under the requested languageId. * - If `undefined` updates the value under the requested languageId only if the configuration is defined for the language. @@ -5113,7 +5125,7 @@ declare module 'vscode' { * - window configuration to workspace folder * - configuration to workspace or workspace folder when no workspace is opened. * - configuration to workspace folder when there is no workspace folder settings. - * - configuration to workspace folder when [WorkspaceConfiguration](#WorkspaceConfiguration) is not scoped to a resource. + * - configuration to workspace folder when {@link WorkspaceConfiguration} is not scoped to a resource. */ update(section: string, value: any, configurationTarget?: ConfigurationTarget | boolean, overrideInLanguage?: boolean): Thenable; @@ -5149,7 +5161,7 @@ declare module 'vscode' { } /** - * Represents the connection of two locations. Provides additional metadata over normal [locations](#Location), + * Represents the connection of two locations. Provides additional metadata over normal {@link Location locations}, * including an origin range. */ export interface LocationLink { @@ -5185,7 +5197,7 @@ declare module 'vscode' { /** * An array of resources for which diagnostics have changed. */ - readonly uris: ReadonlyArray; + readonly uris: readonly Uri[]; } /** @@ -5282,7 +5294,7 @@ declare module 'vscode' { message: string; /** - * The severity, default is [error](#DiagnosticSeverity.Error). + * The severity, default is {@link DiagnosticSeverity.Error error}. */ severity: DiagnosticSeverity; @@ -5294,12 +5306,12 @@ declare module 'vscode' { /** * A code or identifier for this diagnostic. - * Should be used for later processing, e.g. when providing [code actions](#CodeActionContext). + * Should be used for later processing, e.g. when providing {@link CodeActionContext code actions}. */ code?: string | number | { /** * A code or identifier for this diagnostic. - * Should be used for later processing, e.g. when providing [code actions](#CodeActionContext). + * Should be used for later processing, e.g. when providing {@link CodeActionContext code actions}. */ value: string | number; @@ -5325,18 +5337,18 @@ declare module 'vscode' { * * @param range The range to which this diagnostic applies. * @param message The human-readable message. - * @param severity The severity, default is [error](#DiagnosticSeverity.Error). + * @param severity The severity, default is {@link DiagnosticSeverity.Error error}. */ constructor(range: Range, message: string, severity?: DiagnosticSeverity); } /** * A diagnostics collection is a container that manages a set of - * [diagnostics](#Diagnostic). Diagnostics are always scopes to a + * {@link Diagnostic diagnostics}. Diagnostics are always scopes to a * diagnostics collection and a resource. * * To get an instance of a `DiagnosticCollection` use - * [createDiagnosticCollection](#languages.createDiagnosticCollection). + * {@link languages.createDiagnosticCollection createDiagnosticCollection}. */ export interface DiagnosticCollection { @@ -5354,7 +5366,7 @@ declare module 'vscode' { * @param uri A resource identifier. * @param diagnostics Array of diagnostics or `undefined` */ - set(uri: Uri, diagnostics: ReadonlyArray | undefined): void; + set(uri: Uri, diagnostics: readonly Diagnostic[] | undefined): void; /** * Replace diagnostics for multiple resources in this collection. @@ -5366,7 +5378,7 @@ declare module 'vscode' { * * @param entries An array of tuples, like `[[file1, [d1, d2]], [file2, [d3, d4, d5]]]`, or `undefined`. */ - set(entries: ReadonlyArray<[Uri, ReadonlyArray | undefined]>): void; + set(entries: ReadonlyArray<[Uri, readonly Diagnostic[] | undefined]>): void; /** * Remove all diagnostics from this collection that belong @@ -5388,16 +5400,16 @@ declare module 'vscode' { * @param callback Function to execute for each entry. * @param thisArg The `this` context used when invoking the handler function. */ - forEach(callback: (uri: Uri, diagnostics: ReadonlyArray, collection: DiagnosticCollection) => any, thisArg?: any): void; + forEach(callback: (uri: Uri, diagnostics: readonly Diagnostic[], collection: DiagnosticCollection) => any, thisArg?: any): void; /** * Get the diagnostics for a given resource. *Note* that you cannot * modify the diagnostics-array returned from this call. * * @param uri A resource identifier. - * @returns An immutable array of [diagnostics](#Diagnostic) or `undefined`. + * @returns An immutable array of {@link Diagnostic diagnostics} or `undefined`. */ - get(uri: Uri): ReadonlyArray | undefined; + get(uri: Uri): readonly Diagnostic[] | undefined; /** * Check if this collection contains diagnostics for a @@ -5410,7 +5422,7 @@ declare module 'vscode' { /** * Dispose and free associated resources. Calls - * [clear](#DiagnosticCollection.clear). + * {@link DiagnosticCollection.clear clear}. */ dispose(): void; } @@ -5423,13 +5435,13 @@ declare module 'vscode' { export enum ViewColumn { /** * A *symbolic* editor column representing the currently active column. This value - * can be used when opening editors, but the *resolved* [viewColumn](#TextEditor.viewColumn)-value + * can be used when opening editors, but the *resolved* {@link TextEditor.viewColumn viewColumn}-value * of editors will always be `One`, `Two`, `Three`,... or `undefined` but never `Active`. */ Active = -1, /** * A *symbolic* editor column representing the column to the side of the active one. This value - * can be used when opening editors, but the *resolved* [viewColumn](#TextEditor.viewColumn)-value + * can be used when opening editors, but the *resolved* {@link TextEditor.viewColumn viewColumn}-value * of editors will always be `One`, `Two`, `Three`,... or `undefined` but never `Beside`. */ Beside = -2, @@ -5475,7 +5487,7 @@ declare module 'vscode' { * An output channel is a container for readonly textual information. * * To get an instance of an `OutputChannel` use - * [createOutputChannel](#window.createOutputChannel). + * {@link window.createOutputChannel createOutputChannel}. */ export interface OutputChannel { @@ -5544,7 +5556,7 @@ declare module 'vscode' { /** * Role of the widget which defines how a screen reader interacts with it. * The role should be set in special cases when for example a tree-like element behaves like a checkbox. - * If role is not specified VS Code will pick the appropriate role automatically. + * If role is not specified the editor will pick the appropriate role automatically. * More about aria roles can be found here https://w3c.github.io/aria/#widget_roles */ role?: string; @@ -5572,6 +5584,14 @@ declare module 'vscode' { */ export interface StatusBarItem { + /** + * The identifier of this item. + * + * *Note*: if no identifier was provided by the {@link window.createStatusBarItem `window.createStatusBarItem`} + * method, the identifier will match the {@link Extension.id extension identifier}. + */ + readonly id: string; + /** * The alignment of this item. */ @@ -5583,6 +5603,13 @@ declare module 'vscode' { */ readonly priority?: number; + /** + * The name of the entry, like 'Python Language Indicator', 'Git Status' etc. + * Try to keep the length of the name short, yet descriptive enough that + * users can understand what the status bar item is about. + */ + name: string | undefined; + /** * The text to show for the entry. You can embed icons in the text by leveraging the syntax: * @@ -5616,17 +5643,17 @@ declare module 'vscode' { backgroundColor: ThemeColor | undefined; /** - * [`Command`](#Command) or identifier of a command to run on click. + * {@link Command `Command`} or identifier of a command to run on click. * - * The command must be [known](#commands.getCommands). + * The command must be {@link commands.getCommands known}. * - * Note that if this is a [`Command`](#Command) object, only the [`command`](#Command.command) and [`arguments`](#Command.arguments) - * are used by VS Code. + * Note that if this is a {@link Command `Command`} object, only the {@link Command.command `command`} and {@link Command.arguments `arguments`} + * are used by the editor. */ command: string | Command | undefined; /** - * Accessibility information used when screen reader interacts with this StatusBar item + * Accessibility information used when a screen reader interacts with this StatusBar item */ accessibilityInformation?: AccessibilityInformation; @@ -5642,7 +5669,7 @@ declare module 'vscode' { /** * Dispose and free associated resources. Call - * [hide](#StatusBarItem.hide). + * {@link StatusBarItem.hide hide}. */ dispose(): void; } @@ -5767,12 +5794,12 @@ declare module 'vscode' { */ export interface TerminalLink { /** - * The start index of the link on [TerminalLinkContext.line](#TerminalLinkContext.line]. + * The start index of the link on {@link TerminalLinkContext.line}. */ startIndex: number; /** - * The length of the link on [TerminalLinkContext.line](#TerminalLinkContext.line] + * The length of the link on {@link TerminalLinkContext.line}. */ length: number; @@ -5833,7 +5860,7 @@ declare module 'vscode' { * * *Note* that this event should be used to propagate information about children. * - * @see [EventEmitter](#EventEmitter) + * @see {@link EventEmitter} */ onDidChangeFileDecorations?: Event; @@ -5842,7 +5869,7 @@ declare module 'vscode' { * * *Note* that this function is only called when a file gets rendered in the UI. * This means a decoration from a descendent that propagates upwards must be signaled - * to the editor via the [onDidChangeFileDecorations](#FileDecorationProvider.onDidChangeFileDecorations)-event. + * to the editor via the {@link FileDecorationProvider.onDidChangeFileDecorations onDidChangeFileDecorations}-event. * * @param uri The uri of the file to provide a decoration for. * @param token A cancellation token. @@ -5872,7 +5899,7 @@ declare module 'vscode' { /** * Represents an extension. * - * To get an instance of an `Extension` use [getExtension](#extensions.getExtension). + * To get an instance of an `Extension` use {@link extensions.getExtension getExtension}. */ export interface Extension { @@ -5888,7 +5915,7 @@ declare module 'vscode' { /** * The absolute file path of the directory containing this extension. Shorthand - * notation for [Extension.extensionUri.fsPath](#Extension.extensionUri) (independent of the uri scheme). + * notation for {@link Extension.extensionUri Extension.extensionUri.fsPath} (independent of the uri scheme). */ readonly extensionPath: string; @@ -5907,7 +5934,7 @@ declare module 'vscode' { * or if an extension runs where the remote extension host runs. The extension kind * is defined in the `package.json`-file of extensions but can also be refined * via the `remote.extensionKind`-setting. When no remote extension host exists, - * the value is [`ExtensionKind.UI`](#ExtensionKind.UI). + * the value is {@link ExtensionKind.UI `ExtensionKind.UI`}. */ extensionKind: ExtensionKind; @@ -5932,13 +5959,13 @@ declare module 'vscode' { export enum ExtensionMode { /** * The extension is installed normally (for example, from the marketplace - * or VSIX) in VS Code. + * or VSIX) in the editor. */ Production = 1, /** * The extension is running from an `--extensionDevelopmentPath` provided - * when launching VS Code. + * when launching the editor. */ Development = 2, @@ -5966,13 +5993,13 @@ declare module 'vscode' { /** * A memento object that stores state in the context - * of the currently opened [workspace](#workspace.workspaceFolders). + * of the currently opened {@link workspace.workspaceFolders workspace}. */ readonly workspaceState: Memento; /** * A memento object that stores state independent - * of the current opened [workspace](#workspace.workspaceFolders). + * of the current opened {@link workspace.workspaceFolders workspace}. */ readonly globalState: Memento & { /** @@ -5988,11 +6015,12 @@ declare module 'vscode' { * * @param keys The set of keys whose values are synced. */ - setKeysForSync(keys: string[]): void; + setKeysForSync(keys: readonly string[]): void; }; /** - * A storage utility for secrets. + * A storage utility for secrets. Secrets are persisted across reloads and are independent of the + * current opened {@link workspace.workspaceFolders workspace}. */ readonly secrets: SecretStorage; @@ -6003,7 +6031,7 @@ declare module 'vscode' { /** * The absolute file path of the directory containing the extension. Shorthand - * notation for [ExtensionContext.extensionUri.fsPath](#TextDocument.uri) (independent of the uri scheme). + * notation for {@link TextDocument.uri ExtensionContext.extensionUri.fsPath} (independent of the uri scheme). */ readonly extensionPath: string; @@ -6016,8 +6044,8 @@ declare module 'vscode' { /** * Get the absolute path of a resource contained in the extension. * - * *Note* that an absolute uri can be constructed via [`Uri.joinPath`](#Uri.joinPath) and - * [`extensionUri`](#ExtensionContext.extensionUri), e.g. `vscode.Uri.joinPath(context.extensionUri, relativePath);` + * *Note* that an absolute uri can be constructed via {@link Uri.joinPath `Uri.joinPath`} and + * {@link ExtensionContext.extensionUri `extensionUri`}, e.g. `vscode.Uri.joinPath(context.extensionUri, relativePath);` * * @param relativePath A relative path to a resource contained in the extension. * @return The absolute path of the resource. @@ -6030,10 +6058,10 @@ declare module 'vscode' { * up to the extension. However, the parent directory is guaranteed to be existent. * The value is `undefined` when no workspace nor folder has been opened. * - * Use [`workspaceState`](#ExtensionContext.workspaceState) or - * [`globalState`](#ExtensionContext.globalState) to store key value data. + * Use {@link ExtensionContext.workspaceState `workspaceState`} or + * {@link ExtensionContext.globalState `globalState`} to store key value data. * - * @see [`workspace.fs`](#FileSystem) for how to read and write files and folders from + * @see {@link FileSystem `workspace.fs`} for how to read and write files and folders from * an uri. */ readonly storageUri: Uri | undefined; @@ -6043,10 +6071,10 @@ declare module 'vscode' { * can store private state. The directory might not exist on disk and creation is * up to the extension. However, the parent directory is guaranteed to be existent. * - * Use [`workspaceState`](#ExtensionContext.workspaceState) or - * [`globalState`](#ExtensionContext.globalState) to store key value data. + * Use {@link ExtensionContext.workspaceState `workspaceState`} or + * {@link ExtensionContext.globalState `globalState`} to store key value data. * - * @deprecated Use [storageUri](#ExtensionContext.storageUri) instead. + * @deprecated Use {@link ExtensionContext.storageUri storageUri} instead. */ readonly storagePath: string | undefined; @@ -6055,9 +6083,9 @@ declare module 'vscode' { * The directory might not exist on disk and creation is * up to the extension. However, the parent directory is guaranteed to be existent. * - * Use [`globalState`](#ExtensionContext.globalState) to store key value data. + * Use {@link ExtensionContext.globalState `globalState`} to store key value data. * - * @see [`workspace.fs`](#FileSystem) for how to read and write files and folders from + * @see {@link FileSystem `workspace.fs`} for how to read and write files and folders from * an uri. */ readonly globalStorageUri: Uri; @@ -6067,9 +6095,9 @@ declare module 'vscode' { * The directory might not exist on disk and creation is * up to the extension. However, the parent directory is guaranteed to be existent. * - * Use [`globalState`](#ExtensionContext.globalState) to store key value data. + * Use {@link ExtensionContext.globalState `globalState`} to store key value data. * - * @deprecated Use [globalStorageUri](#ExtensionContext.globalStorageUri) instead. + * @deprecated Use {@link ExtensionContext.globalStorageUri globalStorageUri} instead. */ readonly globalStoragePath: string; @@ -6078,7 +6106,7 @@ declare module 'vscode' { * The directory might not exist on disk and creation is up to the extension. However, * the parent directory is guaranteed to be existent. * - * @see [`workspace.fs`](#FileSystem) for how to read and write files and folders from + * @see {@link FileSystem `workspace.fs`} for how to read and write files and folders from * an uri. */ readonly logUri: Uri; @@ -6088,7 +6116,7 @@ declare module 'vscode' { * The directory might not exist on disk and creation is up to the extension. However, * the parent directory is guaranteed to be existent. * - * @deprecated Use [logUri](#ExtensionContext.logUri) instead. + * @deprecated Use {@link ExtensionContext.logUri logUri} instead. */ readonly logPath: string; @@ -6521,7 +6549,7 @@ declare module 'vscode' { constructor(commandLine: string, options?: ShellExecutionOptions); /** - * Creates a shell execution with a command and arguments. For the real execution VS Code will + * Creates a shell execution with a command and arguments. For the real execution the editor will * construct a command line from the command and the arguments. This is subject to interpretation * especially when it comes to quoting. If full control over the command line is needed please * use the constructor that creates a `ShellExecution` with the full command line. @@ -6561,9 +6589,9 @@ declare module 'vscode' { /** * Constructs a CustomExecution task object. The callback will be executed when the task is run, at which point the * extension should return the Pseudoterminal it will "run in". The task should wait to do further execution until - * [Pseudoterminal.open](#Pseudoterminal.open) is called. Task cancellation should be handled using - * [Pseudoterminal.close](#Pseudoterminal.close). When the task is complete fire - * [Pseudoterminal.onDidClose](#Pseudoterminal.onDidClose). + * {@link Pseudoterminal.open} is called. Task cancellation should be handled using + * {@link Pseudoterminal.close}. When the task is complete fire + * {@link Pseudoterminal.onDidClose}. * @param callback The callback that will be called when the task is started by a user. Any ${} style variables that * were in the task definition will be resolved and passed into the callback as `resolvedDefinition`. */ @@ -6646,7 +6674,7 @@ declare module 'vscode' { /** * A human-readable string which is rendered less prominently on a separate line in places - * where the task's name is displayed. Supports rendering of [theme icons](#ThemeIcon) + * where the task's name is displayed. Supports rendering of {@link ThemeIcon theme icons} * via the `$()`-syntax. */ detail?: string; @@ -6663,7 +6691,7 @@ declare module 'vscode' { /** * A human-readable string describing the source of this shell task, e.g. 'gulp' - * or 'npm'. Supports rendering of [theme icons](#ThemeIcon) via the `$()`-syntax. + * or 'npm'. Supports rendering of {@link ThemeIcon theme icons} via the `$()`-syntax. */ source: string; @@ -6694,7 +6722,7 @@ declare module 'vscode' { /** * A task provider allows to add tasks to the task service. - * A task provider is registered via #tasks.registerTaskProvider. + * A task provider is registered via {@link tasks.registerTaskProvider}. */ export interface TaskProvider { /** @@ -6705,7 +6733,7 @@ declare module 'vscode' { provideTasks(token: CancellationToken): ProviderResult; /** - * Resolves a task that has no [`execution`](#Task.execution) set. Tasks are + * Resolves a task that has no {@link Task.execution `execution`} set. Tasks are * often created from information found in the `tasks.json`-file. Such tasks miss * the information on how to execute them and a task provider must fill in * the missing information in the `resolveTask`-method. This method will not be @@ -6823,7 +6851,7 @@ declare module 'vscode' { * * @param type The task kind type this provider is registered for. * @param provider A task provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerTaskProvider(type: string, provider: TaskProvider): Disposable; @@ -6837,7 +6865,7 @@ declare module 'vscode' { export function fetchTasks(filter?: TaskFilter): Thenable; /** - * Executes a task that is managed by VS Code. The returned + * Executes a task that is managed by the editor. The returned * task execution can be used to terminate the task. * * @throws When running a ShellExecution or a ProcessExecution @@ -6851,7 +6879,7 @@ declare module 'vscode' { /** * The currently active task executions or an empty array. */ - export const taskExecutions: ReadonlyArray; + export const taskExecutions: readonly TaskExecution[]; /** * Fires when a task starts. @@ -6991,7 +7019,7 @@ declare module 'vscode' { /** * A code that identifies this error. * - * Possible values are names of errors, like [`FileNotFound`](#FileSystemError.FileNotFound), + * Possible values are names of errors, like {@link FileSystemError.FileNotFound `FileNotFound`}, * or `Unknown` for unspecified errors. */ readonly code: string; @@ -7039,22 +7067,22 @@ declare module 'vscode' { * and to manage files and folders. It allows extensions to serve files from remote places, * like ftp-servers, and to seamlessly integrate those into the editor. * - * * *Note 1:* The filesystem provider API works with [uris](#Uri) and assumes hierarchical + * * *Note 1:* The filesystem provider API works with {@link Uri uris} and assumes hierarchical * paths, e.g. `foo:/my/path` is a child of `foo:/my/` and a parent of `foo:/my/path/deeper`. * * *Note 2:* There is an activation event `onFileSystem:` that fires when a file * or folder is being accessed. - * * *Note 3:* The word 'file' is often used to denote all [kinds](#FileType) of files, e.g. + * * *Note 3:* The word 'file' is often used to denote all {@link FileType kinds} of files, e.g. * folders, symbolic links, and regular files. */ export interface FileSystemProvider { /** * An event to signal that a resource has been created, changed, or deleted. This - * event should fire for resources that are being [watched](#FileSystemProvider.watch) + * event should fire for resources that are being {@link FileSystemProvider.watch watched} * by clients of this provider. * * *Note:* It is important that the metadata of the file that changed provides an - * updated `mtime` that advanced from the previous value in the [stat](#FileStat) and a + * updated `mtime` that advanced from the previous value in the {@link FileStat stat} and a * correct `size` value. Otherwise there may be optimizations in place that will not show * the change in an editor for example. */ @@ -7077,21 +7105,21 @@ declare module 'vscode' { * Retrieve metadata about a file. * * Note that the metadata for symbolic links should be the metadata of the file they refer to. - * Still, the [SymbolicLink](#FileType.SymbolicLink)-type must be used in addition to the actual type, e.g. + * Still, the {@link FileType.SymbolicLink SymbolicLink}-type must be used in addition to the actual type, e.g. * `FileType.SymbolicLink | FileType.Directory`. * * @param uri The uri of the file to retrieve metadata about. * @return The file metadata about the file. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `uri` doesn't exist. + * @throws {@link FileSystemError.FileNotFound `FileNotFound`} when `uri` doesn't exist. */ stat(uri: Uri): FileStat | Thenable; /** - * Retrieve all entries of a [directory](#FileType.Directory). + * Retrieve all entries of a {@link FileType.Directory directory}. * * @param uri The uri of the folder. * @return An array of name/type-tuples or a thenable that resolves to such. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `uri` doesn't exist. + * @throws {@link FileSystemError.FileNotFound `FileNotFound`} when `uri` doesn't exist. */ readDirectory(uri: Uri): [string, FileType][] | Thenable<[string, FileType][]>; @@ -7099,9 +7127,9 @@ declare module 'vscode' { * Create a new directory (Note, that new files are created via `write`-calls). * * @param uri The uri of the new folder. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when the parent of `uri` doesn't exist, e.g. no mkdirp-logic required. - * @throws [`FileExists`](#FileSystemError.FileExists) when `uri` already exists. - * @throws [`NoPermissions`](#FileSystemError.NoPermissions) when permissions aren't sufficient. + * @throws {@link FileSystemError.FileNotFound `FileNotFound`} when the parent of `uri` doesn't exist, e.g. no mkdirp-logic required. + * @throws {@link FileSystemError.FileExists `FileExists`} when `uri` already exists. + * @throws {@link FileSystemError.NoPermissions `NoPermissions`} when permissions aren't sufficient. */ createDirectory(uri: Uri): void | Thenable; @@ -7110,7 +7138,7 @@ declare module 'vscode' { * * @param uri The uri of the file. * @return An array of bytes or a thenable that resolves to such. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `uri` doesn't exist. + * @throws {@link FileSystemError.FileNotFound `FileNotFound`} when `uri` doesn't exist. */ readFile(uri: Uri): Uint8Array | Thenable; @@ -7120,10 +7148,10 @@ declare module 'vscode' { * @param uri The uri of the file. * @param content The new content of the file. * @param options Defines if missing files should or must be created. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `uri` doesn't exist and `create` is not set. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when the parent of `uri` doesn't exist and `create` is set, e.g. no mkdirp-logic required. - * @throws [`FileExists`](#FileSystemError.FileExists) when `uri` already exists, `create` is set but `overwrite` is not set. - * @throws [`NoPermissions`](#FileSystemError.NoPermissions) when permissions aren't sufficient. + * @throws {@link FileSystemError.FileNotFound `FileNotFound`} when `uri` doesn't exist and `create` is not set. + * @throws {@link FileSystemError.FileNotFound `FileNotFound`} when the parent of `uri` doesn't exist and `create` is set, e.g. no mkdirp-logic required. + * @throws {@link FileSystemError.FileExists `FileExists`} when `uri` already exists, `create` is set but `overwrite` is not set. + * @throws {@link FileSystemError.NoPermissions `NoPermissions`} when permissions aren't sufficient. */ writeFile(uri: Uri, content: Uint8Array, options: { create: boolean, overwrite: boolean }): void | Thenable; @@ -7132,8 +7160,8 @@ declare module 'vscode' { * * @param uri The resource that is to be deleted. * @param options Defines if deletion of folders is recursive. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `uri` doesn't exist. - * @throws [`NoPermissions`](#FileSystemError.NoPermissions) when permissions aren't sufficient. + * @throws {@link FileSystemError.FileNotFound `FileNotFound`} when `uri` doesn't exist. + * @throws {@link FileSystemError.NoPermissions `NoPermissions`} when permissions aren't sufficient. */ delete(uri: Uri, options: { recursive: boolean }): void | Thenable; @@ -7143,10 +7171,10 @@ declare module 'vscode' { * @param oldUri The existing file. * @param newUri The new location. * @param options Defines if existing files should be overwritten. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `oldUri` doesn't exist. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when parent of `newUri` doesn't exist, e.g. no mkdirp-logic required. - * @throws [`FileExists`](#FileSystemError.FileExists) when `newUri` exists and when the `overwrite` option is not `true`. - * @throws [`NoPermissions`](#FileSystemError.NoPermissions) when permissions aren't sufficient. + * @throws {@link FileSystemError.FileNotFound `FileNotFound`} when `oldUri` doesn't exist. + * @throws {@link FileSystemError.FileNotFound `FileNotFound`} when parent of `newUri` doesn't exist, e.g. no mkdirp-logic required. + * @throws {@link FileSystemError.FileExists `FileExists`} when `newUri` exists and when the `overwrite` option is not `true`. + * @throws {@link FileSystemError.NoPermissions `NoPermissions`} when permissions aren't sufficient. */ rename(oldUri: Uri, newUri: Uri, options: { overwrite: boolean }): void | Thenable; @@ -7157,21 +7185,21 @@ declare module 'vscode' { * @param source The existing file. * @param destination The destination location. * @param options Defines if existing files should be overwritten. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `source` doesn't exist. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when parent of `destination` doesn't exist, e.g. no mkdirp-logic required. - * @throws [`FileExists`](#FileSystemError.FileExists) when `destination` exists and when the `overwrite` option is not `true`. - * @throws [`NoPermissions`](#FileSystemError.NoPermissions) when permissions aren't sufficient. + * @throws {@link FileSystemError.FileNotFound `FileNotFound`} when `source` doesn't exist. + * @throws {@link FileSystemError.FileNotFound `FileNotFound`} when parent of `destination` doesn't exist, e.g. no mkdirp-logic required. + * @throws {@link FileSystemError.FileExists `FileExists`} when `destination` exists and when the `overwrite` option is not `true`. + * @throws {@link FileSystemError.NoPermissions `NoPermissions`} when permissions aren't sufficient. */ copy?(source: Uri, destination: Uri, options: { overwrite: boolean }): void | Thenable; } /** * The file system interface exposes the editor's built-in and contributed - * [file system providers](#FileSystemProvider). It allows extensions to work + * {@link FileSystemProvider file system providers}. It allows extensions to work * with files from the local disk as well as files from remote places, like the * remote extension host or ftp-servers. * - * *Note* that an instance of this interface is available as [`workspace.fs`](#workspace.fs). + * *Note* that an instance of this interface is available as {@link workspace.fs `workspace.fs`}. */ export interface FileSystem { @@ -7184,7 +7212,7 @@ declare module 'vscode' { stat(uri: Uri): Thenable; /** - * Retrieve all entries of a [directory](#FileType.Directory). + * Retrieve all entries of a {@link FileType.Directory directory}. * * @param uri The uri of the folder. * @return An array of name/type-tuples or a thenable that resolves to such. @@ -7253,7 +7281,7 @@ declare module 'vscode' { * @param scheme The scheme of the filesystem, for example `file` or `git`. * * @return `true` if the file system supports writing, `false` if it does not - * support writing (i.e. it is readonly), and `undefined` if VS Code does not + * support writing (i.e. it is readonly), and `undefined` if the editor does not * know about the filesystem. */ isWritableFileSystem(scheme: string): boolean | undefined; @@ -7299,7 +7327,7 @@ declare module 'vscode' { * * Pass in an empty array to disallow access to any local resources. */ - readonly localResourceRoots?: ReadonlyArray; + readonly localResourceRoots?: readonly Uri[]; /** * Mappings of localhost ports used inside the webview. @@ -7314,7 +7342,7 @@ declare module 'vscode' { * *Note* that port mappings only work for `http` or `https` urls. Websocket urls (e.g. `ws://localhost:3000`) * cannot be mapped to another port. */ - readonly portMapping?: ReadonlyArray; + readonly portMapping?: readonly WebviewPortMapping[]; } /** @@ -7332,9 +7360,9 @@ declare module 'vscode' { * This should be a complete, valid html document. Changing this property causes the webview to be reloaded. * * Webviews are sandboxed from normal extension process, so all communication with the webview must use - * message passing. To send a message from the extension to the webview, use [`postMessage`](#Webview.postMessage). + * message passing. To send a message from the extension to the webview, use {@link Webview.postMessage `postMessage`}. * To send message from the webview back to an extension, use the `acquireVsCodeApi` function inside the webview - * to get a handle to VS Code's api and then call `.postMessage()`: + * to get a handle to the editor's api and then call `.postMessage()`: * * ```html * * ``` * - * To load a resources from the workspace inside a webview, use the `[asWebviewUri](#Webview.asWebviewUri)` method - * and ensure the resource's directory is listed in [`WebviewOptions.localResourceRoots`](#WebviewOptions.localResourceRoots). + * To load a resources from the workspace inside a webview, use the `{@link Webview.asWebviewUri asWebviewUri}` method + * and ensure the resource's directory is listed in {@link WebviewOptions.localResourceRoots `WebviewOptions.localResourceRoots`}. * * Keep in mind that even though webviews are sandboxed, they still allow running scripts and loading arbitrary content, * so extensions must follow all standard web security best practices when working with webviews. This includes @@ -7356,7 +7384,7 @@ declare module 'vscode' { /** * Fired when the webview content posts a message. * - * Webview content can post strings or json serializable objects back to a VS Code extension. They cannot + * Webview content can post strings or json serializable objects back to an extension. They cannot * post `Blob`, `File`, `ImageData` and other DOM specific objects since the extension that receives the * message does not run in a browser environment. */ @@ -7369,6 +7397,16 @@ declare module 'vscode' { * background with `retainContextWhenHidden`). * * @param message Body of the message. This must be a string or other json serializable object. + * + * For older versions of vscode, if an `ArrayBuffer` is included in `message`, + * it will not be serialized properly and will not be received by the webview. + * Similarly any TypedArrays, such as a `Uint8Array`, will be very inefficiently + * serialized and will also not be recreated as a typed array inside the webview. + * + * However if your extension targets vscode 1.57+ in the `engines` field of its + * `package.json`, any `ArrayBuffer` values that appear in `message` will be more + * efficiently transferred to the webview and will also be correctly recreated inside + * of the webview. */ postMessage(message: any): Thenable; @@ -7414,7 +7452,7 @@ declare module 'vscode' { * * Normally the webview panel's html context is created when the panel becomes visible * and destroyed when it is hidden. Extensions that have complex state - * or UI can set the `retainContextWhenHidden` to make VS Code keep the webview + * or UI can set the `retainContextWhenHidden` to make the editor keep the webview * context around, even when the webview moves to a background tab. When a webview using * `retainContextWhenHidden` becomes hidden, its scripts and other dynamic content are suspended. * When the panel becomes visible again, the context is automatically restored @@ -7447,7 +7485,7 @@ declare module 'vscode' { iconPath?: Uri | { light: Uri; dark: Uri }; /** - * [`Webview`](#Webview) belonging to the panel. + * {@link Webview `Webview`} belonging to the panel. */ readonly webview: Webview; @@ -7524,7 +7562,7 @@ declare module 'vscode' { * There are two types of webview persistence: * * - Persistence within a session. - * - Persistence across sessions (across restarts of VS Code). + * - Persistence across sessions (across restarts of the editor). * * A `WebviewPanelSerializer` is only required for the second case: persisting a webview across sessions. * @@ -7544,8 +7582,8 @@ declare module 'vscode' { * setState({ value: oldState.value + 1 }) * ``` * - * A `WebviewPanelSerializer` extends this persistence across restarts of VS Code. When the editor is shutdown, - * VS Code will save off the state from `setState` of all webviews that have a serializer. When the + * A `WebviewPanelSerializer` extends this persistence across restarts of the editor. When the editor is shutdown, + * it will save off the state from `setState` of all webviews that have a serializer. When the * webview first becomes visible after the restart, this state is passed to `deserializeWebviewPanel`. * The extension can then restore the old `WebviewPanel` from this state. * @@ -7640,7 +7678,7 @@ declare module 'vscode' { /** * Persisted state from the webview content. * - * To save resources, VS Code normally deallocates webview documents (the iframe content) that are not visible. + * To save resources, the editor normally deallocates webview documents (the iframe content) that are not visible. * For example, when the user collapse a view or switches to another top level activity in the sidebar, the * `WebviewView` itself is kept alive but the webview's underlying document is deallocated. It is recreated when * the view becomes visible again. @@ -7663,7 +7701,7 @@ declare module 'vscode' { * setState({ value: oldState.value + 1 }) * ``` * - * VS Code ensures that the persisted state is saved correctly when a webview is hidden and across + * The editor ensures that the persisted state is saved correctly when a webview is hidden and across * editor restarts. */ readonly state: T | undefined; @@ -7692,8 +7730,8 @@ declare module 'vscode' { /** * Provider for text based custom editors. * - * Text based custom editors use a [`TextDocument`](#TextDocument) as their data model. This considerably simplifies - * implementing a custom editor as it allows VS Code to handle many common operations such as + * Text based custom editors use a {@link TextDocument `TextDocument`} as their data model. This considerably simplifies + * implementing a custom editor as it allows the editor to handle many common operations such as * undo and backup. The provider is responsible for synchronizing text changes between the webview and the `TextDocument`. */ export interface CustomTextEditorProvider { @@ -7711,7 +7749,7 @@ declare module 'vscode' { * * During resolve, the provider must fill in the initial html for the content webview panel and hook up all * the event listeners on it that it is interested in. The provider can also hold onto the `WebviewPanel` to - * use later for example in a command. See [`WebviewPanel`](#WebviewPanel) for additional details. + * use later for example in a command. See {@link WebviewPanel `WebviewPanel`} for additional details. * * @param token A cancellation token that indicates the result is no longer needed. * @@ -7721,10 +7759,10 @@ declare module 'vscode' { } /** - * Represents a custom document used by a [`CustomEditorProvider`](#CustomEditorProvider). + * Represents a custom document used by a {@link CustomEditorProvider `CustomEditorProvider`}. * * Custom documents are only used within a given `CustomEditorProvider`. The lifecycle of a `CustomDocument` is - * managed by VS Code. When no more references remain to a `CustomDocument`, it is disposed of. + * managed by the editor. When no more references remain to a `CustomDocument`, it is disposed of. */ interface CustomDocument { /** @@ -7735,16 +7773,16 @@ declare module 'vscode' { /** * Dispose of the custom document. * - * This is invoked by VS Code when there are no more references to a given `CustomDocument` (for example when + * This is invoked by the editor when there are no more references to a given `CustomDocument` (for example when * all editors associated with the document have been closed.) */ dispose(): void; } /** - * Event triggered by extensions to signal to VS Code that an edit has occurred on an [`CustomDocument`](#CustomDocument). + * Event triggered by extensions to signal to the editor that an edit has occurred on an {@link CustomDocument `CustomDocument`}. * - * @see [`CustomDocumentProvider.onDidChangeCustomDocument`](#CustomDocumentProvider.onDidChangeCustomDocument). + * @see {@link CustomEditorProvider.onDidChangeCustomDocument `CustomEditorProvider.onDidChangeCustomDocument`}. */ interface CustomDocumentEditEvent { @@ -7756,18 +7794,18 @@ declare module 'vscode' { /** * Undo the edit operation. * - * This is invoked by VS Code when the user undoes this edit. To implement `undo`, your + * This is invoked by the editor 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`. + * edit was added to the editor'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 + * This is invoked by the editor 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`. + * edit was added to the editor's internal edit stack by `onDidChangeCustomDocument`. */ redo(): Thenable | void; @@ -7780,10 +7818,10 @@ declare module 'vscode' { } /** - * Event triggered by extensions to signal to VS Code that the content of a [`CustomDocument`](#CustomDocument) + * Event triggered by extensions to signal to the editor that the content of a {@link CustomDocument `CustomDocument`} * has changed. * - * @see [`CustomDocumentProvider.onDidChangeCustomDocument`](#CustomDocumentProvider.onDidChangeCustomDocument). + * @see {@link CustomEditorProvider.onDidChangeCustomDocument `CustomEditorProvider.onDidChangeCustomDocument`}. */ interface CustomDocumentContentChangeEvent { /** @@ -7793,7 +7831,7 @@ declare module 'vscode' { } /** - * A backup for an [`CustomDocument`](#CustomDocument). + * A backup for an {@link CustomDocument `CustomDocument`}. */ interface CustomDocumentBackup { /** @@ -7806,14 +7844,14 @@ declare module 'vscode' { /** * 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 + * This is called by the editor 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; } /** - * Additional information used to implement [`CustomEditableDocument.backup`](#CustomEditableDocument.backup). + * Additional information used to implement {@link CustomEditableDocument.backup `CustomEditableDocument.backup`}. */ interface CustomDocumentBackupContext { /** @@ -7851,10 +7889,10 @@ declare module 'vscode' { /** * Provider for readonly custom editors that use a custom document model. * - * Custom editors use [`CustomDocument`](#CustomDocument) as their document model instead of a [`TextDocument`](#TextDocument). + * Custom editors use {@link CustomDocument `CustomDocument`} as their document model instead of a {@link TextDocument `TextDocument`}. * * You should use this type of custom editor when dealing with binary files or more complex scenarios. For simple - * text based documents, use [`CustomTextEditorProvider`](#CustomTextEditorProvider) instead. + * text based documents, use {@link CustomTextEditorProvider `CustomTextEditorProvider`} instead. * * @param T Type of the custom document returned by this provider. */ @@ -7889,7 +7927,7 @@ declare module 'vscode' { * * During resolve, the provider must fill in the initial html for the content webview panel and hook up all * the event listeners on it that it is interested in. The provider can also hold onto the `WebviewPanel` to - * use later for example in a command. See [`WebviewPanel`](#WebviewPanel) for additional details. + * use later for example in a command. See {@link WebviewPanel `WebviewPanel`} for additional details. * * @param token A cancellation token that indicates the result is no longer needed. * @@ -7901,11 +7939,11 @@ declare module 'vscode' { /** * Provider for editable custom editors that use a custom document model. * - * Custom editors use [`CustomDocument`](#CustomDocument) as their document model instead of a [`TextDocument`](#TextDocument). + * Custom editors use {@link CustomDocument `CustomDocument`} as their document model instead of a {@link TextDocument `TextDocument`}. * This gives extensions full control over actions such as edit, save, and backup. * * You should use this type of custom editor when dealing with binary files or more complex scenarios. For simple - * text based documents, use [`CustomTextEditorProvider`](#CustomTextEditorProvider) instead. + * text based documents, use {@link CustomTextEditorProvider `CustomTextEditorProvider`} instead. * * @param T Type of the custom document returned by this provider. */ @@ -7917,14 +7955,14 @@ declare module 'vscode' { * anything from changing some text, to cropping an image, to reordering a list. Your extension is free to * define what an edit is and what data is stored on each edit. * - * Firing `onDidChange` causes VS Code to mark the editors as being dirty. This is cleared when the user either + * Firing `onDidChange` causes the editors to be marked as being dirty. This is cleared when the user either * saves or reverts the file. * * Editors that support undo/redo must fire a `CustomDocumentEditEvent` whenever an edit happens. This allows - * users to undo and redo the edit using VS Code's standard VS Code keyboard shortcuts. VS Code will also mark + * users to undo and redo the edit using the editor's standard keyboard shortcuts. The editor will also mark * the editor as no longer being dirty if the user undoes all edits to the last saved state. * - * Editors that support editing but cannot use VS Code's standard undo/redo mechanism must fire a `CustomDocumentContentChangeEvent`. + * Editors that support editing but cannot use the editor's standard undo/redo mechanism must fire a `CustomDocumentContentChangeEvent`. * The only way for a user to clear the dirty state of an editor that does not support undo/redo is to either * `save` or `revert` the file. * @@ -7935,7 +7973,7 @@ declare module 'vscode' { /** * Save a custom document. * - * This method is invoked by VS Code when the user saves a custom editor. This can happen when the user + * This method is invoked by the editor when the user saves a custom editor. This can happen when the user * triggers save while the custom editor is active, by commands such as `save all`, or by auto save if enabled. * * To implement `save`, the implementer must persist the custom editor. This usually means writing the @@ -7952,7 +7990,7 @@ declare module 'vscode' { /** * Save a custom document to a different location. * - * This method is invoked by VS Code when the user triggers 'save as' on a custom editor. The implementer must + * This method is invoked by the editor when the user triggers 'save as' on a custom editor. The implementer must * persist the custom editor to `destination`. * * When the user accepts save as, the current editor is be replaced by an non-dirty editor for the newly saved file. @@ -7968,8 +8006,8 @@ declare module 'vscode' { /** * Revert a custom document to its last saved state. * - * This method is invoked by VS Code when the user triggers `File: Revert File` in a custom editor. (Note that - * this is only used using VS Code's `File: Revert File` command and not on a `git revert` of the file). + * This method is invoked by the editor when the user triggers `File: Revert File` in a custom editor. (Note that + * this is only used using the editor's `File: Revert File` command and not on a `git revert` of the file). * * To implement `revert`, the implementer must make sure all editor instances (webviews) for `document` * are displaying the document in the same state is saved in. This usually means reloading the file from the @@ -7987,7 +8025,7 @@ declare module 'vscode' { * * Backups are used for hot exit and to prevent data loss. Your `backup` method should persist the resource in * its current state, i.e. with the edits applied. Most commonly this means saving the resource to disk in - * the `ExtensionContext.storagePath`. When VS Code reloads and your custom editor is opened for a resource, + * the `ExtensionContext.storagePath`. When the editor reloads and your custom editor is opened for a resource, * 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. * @@ -8001,7 +8039,7 @@ declare module 'vscode' { * @param cancellation Token that signals the current backup since a new backup is coming in. It is up to your * extension to decided how to respond to cancellation. If for example your extension is backing up a large file * in an operation that takes time to complete, your extension may decide to finish the ongoing backup rather - * than cancelling it to ensure that VS Code has some valid backup. + * than cancelling it to ensure that the editor has some valid backup. */ backupCustomDocument(document: T, context: CustomDocumentBackupContext, cancellation: CancellationToken): Thenable; } @@ -8097,7 +8135,7 @@ declare module 'vscode' { export const isTelemetryEnabled: boolean; /** - * An [event](#Event) which fires when the user enabled or disables telemetry. + * An {@link Event} which fires when the user enabled or disables telemetry. * `true` if the user has enabled telemetry or `false` if the user has disabled telemetry. */ export const onDidChangeTelemetryEnabled: Event; @@ -8108,7 +8146,7 @@ declare module 'vscode' { * * *Note* that the value is `undefined` when there is no remote extension host but that the * value is defined in all extension hosts (local and remote) in case a remote extension host - * exists. Use [`Extension#extensionKind`](#Extension.extensionKind) to know if + * exists. Use {@link Extension.extensionKind} to know if * a specific extension runs remote or not. */ export const remoteName: string | undefined; @@ -8134,7 +8172,7 @@ declare module 'vscode' { * * a mail client (`mailto:`) * * VSCode itself (`vscode:` from `vscode.env.uriScheme`) * - * *Note* that [`showTextDocument`](#window.showTextDocument) is the right + * *Note* that {@link window.showTextDocument `showTextDocument`} is the right * way to open a text document inside the editor, not this function. * * @param target The uri that should be opened. @@ -8155,13 +8193,13 @@ declare module 'vscode' { * * If the extension is running remotely, this function automatically establishes a port forwarding tunnel * from the local machine to `target` on the remote and returns a local uri to the tunnel. The lifetime of - * the port forwarding tunnel is managed by VS Code and the tunnel can be closed by the user. + * the port forwarding tunnel is managed by the editor and the tunnel can be closed by the user. * * *Note* that uris passed through `openExternal` are automatically resolved and you should not call `asExternalUri` on them. * * #### `vscode.env.uriScheme` * - * Creates a uri that - if opened in a browser (e.g. via `openExternal`) - will result in a registered [UriHandler](#UriHandler) + * Creates a uri that - if opened in a browser (e.g. via `openExternal`) - will result in a registered {@link UriHandler} * to trigger. * * Extensions should not make any assumptions about the resulting uri and should not alter it in anyway. @@ -8169,7 +8207,7 @@ declare module 'vscode' { * argument to the server to authenticate to. * * *Note* that if the server decides to add additional query parameters to the uri (e.g. a token or secret), it - * will appear in the uri that is passed to the [UriHandler](#UriHandler). + * will appear in the uri that is passed to the {@link UriHandler}. * * **Example** of an authentication flow: * ```typescript @@ -8198,9 +8236,9 @@ declare module 'vscode' { * Namespace for dealing with commands. In short, a command is a function with a * unique identifier. The function is sometimes also called _command handler_. * - * Commands can be added to the editor using the [registerCommand](#commands.registerCommand) - * and [registerTextEditorCommand](#commands.registerTextEditorCommand) functions. Commands - * can be executed [manually](#commands.executeCommand) or from a UI gesture. Those are: + * Commands can be added to the editor using the {@link commands.registerCommand registerCommand} + * and {@link commands.registerTextEditorCommand registerTextEditorCommand} functions. Commands + * can be executed {@link commands.executeCommand manually} or from a UI gesture. Those are: * * * palette - Use the `commands`-section in `package.json` to make a command show in * the [command palette](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette). @@ -8250,14 +8288,14 @@ declare module 'vscode' { * Registers a text editor command that can be invoked via a keyboard shortcut, * a menu item, an action, or directly. * - * Text editor commands are different from ordinary [commands](#commands.registerCommand) as + * Text editor commands are different from ordinary {@link commands.registerCommand commands} as * they only execute when there is an active editor when the command is called. Also, the * command handler of an editor command has access to the active editor and to an - * [edit](#TextEditorEdit)-builder. Note that the edit-builder is only valid while the + * {@link TextEditorEdit edit}-builder. Note that the edit-builder is only valid while the * callback executes. * * @param command A unique identifier for the command. - * @param callback A command handler function with access to an [editor](#TextEditor) and an [edit](#TextEditorEdit). + * @param callback A command handler function with access to an {@link TextEditor editor} and an {@link TextEditorEdit edit}. * @param thisArg The `this` context used when invoking the handler function. * @return Disposable which unregisters this command on disposal. */ @@ -8268,7 +8306,7 @@ declare module 'vscode' { * * * *Note 1:* When executing an editor command not all types are allowed to * be passed as arguments. Allowed are the primitive types `string`, `boolean`, - * `number`, `undefined`, and `null`, as well as [`Position`](#Position), [`Range`](#Range), [`Uri`](#Uri) and [`Location`](#Location). + * `number`, `undefined`, and `null`, as well as {@link Position `Position`}, {@link Range `Range`}, {@link Uri `Uri`} and {@link Location `Location`}. * * *Note 2:* There are no restrictions when executing commands that have been contributed * by extensions. * @@ -8301,16 +8339,16 @@ declare module 'vscode' { } /** - * A uri handler is responsible for handling system-wide [uris](#Uri). + * A uri handler is responsible for handling system-wide {@link Uri uris}. * - * @see [window.registerUriHandler](#window.registerUriHandler). + * @see {@link window.registerUriHandler}. */ export interface UriHandler { /** - * Handle the provided system-wide [uri](#Uri). + * Handle the provided system-wide {@link Uri}. * - * @see [window.registerUriHandler](#window.registerUriHandler). + * @see {@link window.registerUriHandler}. */ handleUri(uri: Uri): ProviderResult; } @@ -8335,42 +8373,42 @@ declare module 'vscode' { export let visibleTextEditors: TextEditor[]; /** - * An [event](#Event) which fires when the [active editor](#window.activeTextEditor) + * An {@link Event} which fires when the {@link window.activeTextEditor active editor} * has changed. *Note* that the event also fires when the active editor changes * to `undefined`. */ export const onDidChangeActiveTextEditor: Event; /** - * An [event](#Event) which fires when the array of [visible editors](#window.visibleTextEditors) + * An {@link Event} which fires when the array of {@link window.visibleTextEditors visible editors} * has changed. */ export const onDidChangeVisibleTextEditors: Event; /** - * An [event](#Event) which fires when the selection in an editor has changed. + * An {@link Event} which fires when the selection in an editor has changed. */ export const onDidChangeTextEditorSelection: Event; /** - * An [event](#Event) which fires when the visible ranges of an editor has changed. + * An {@link Event} which fires when the visible ranges of an editor has changed. */ export const onDidChangeTextEditorVisibleRanges: Event; /** - * An [event](#Event) which fires when the options of an editor have changed. + * An {@link Event} which fires when the options of an editor have changed. */ export const onDidChangeTextEditorOptions: Event; /** - * An [event](#Event) which fires when the view column of an editor has changed. + * An {@link Event} which fires when the view column of an editor has changed. */ export const onDidChangeTextEditorViewColumn: Event; /** * The currently opened terminals or an empty array. */ - export const terminals: ReadonlyArray; + export const terminals: readonly Terminal[]; /** * The currently active terminal or `undefined`. The active terminal is the one that @@ -8379,20 +8417,20 @@ declare module 'vscode' { export const activeTerminal: Terminal | undefined; /** - * An [event](#Event) which fires when the [active terminal](#window.activeTerminal) + * An {@link Event} which fires when the {@link window.activeTerminal active terminal} * has changed. *Note* that the event also fires when the active terminal changes * to `undefined`. */ export const onDidChangeActiveTerminal: Event; /** - * An [event](#Event) which fires when a terminal has been created, either through the - * [createTerminal](#window.createTerminal) API or commands. + * An {@link Event} which fires when a terminal has been created, either through the + * {@link window.createTerminal createTerminal} API or commands. */ export const onDidOpenTerminal: Event; /** - * An [event](#Event) which fires when a terminal is disposed. + * An {@link Event} which fires when a terminal is disposed. */ export const onDidCloseTerminal: Event; @@ -8402,42 +8440,42 @@ declare module 'vscode' { export const state: WindowState; /** - * An [event](#Event) which fires when the focus state of the current window + * An {@link Event} which fires when the focus state of the current window * changes. The value of the event represents whether the window is focused. */ export const onDidChangeWindowState: Event; /** - * Show the given document in a text editor. A [column](#ViewColumn) can be provided - * to control where the editor is being shown. Might change the [active editor](#window.activeTextEditor). + * Show the given document in a text editor. A {@link ViewColumn column} can be provided + * to control where the editor is being shown. Might change the {@link window.activeTextEditor active editor}. * * @param document A text document to be shown. - * @param column A view column in which the [editor](#TextEditor) should be shown. The default is the [active](#ViewColumn.Active), other values - * are adjusted to be `Min(column, columnCount + 1)`, the [active](#ViewColumn.Active)-column is not adjusted. Use [`ViewColumn.Beside`](#ViewColumn.Beside) + * @param column A view column in which the {@link TextEditor editor} should be shown. The default is the {@link ViewColumn.Active active}, other values + * are adjusted to be `Min(column, columnCount + 1)`, the {@link ViewColumn.Active active}-column is not adjusted. Use {@link ViewColumn.Beside `ViewColumn.Beside`} * to open the editor to the side of the currently active one. * @param preserveFocus When `true` the editor will not take focus. - * @return A promise that resolves to an [editor](#TextEditor). + * @return A promise that resolves to an {@link TextEditor editor}. */ export function showTextDocument(document: TextDocument, column?: ViewColumn, preserveFocus?: boolean): Thenable; /** - * Show the given document in a text editor. [Options](#TextDocumentShowOptions) can be provided - * to control options of the editor is being shown. Might change the [active editor](#window.activeTextEditor). + * Show the given document in a text editor. {@link TextDocumentShowOptions Options} can be provided + * to control options of the editor is being shown. Might change the {@link window.activeTextEditor active editor}. * * @param document A text document to be shown. - * @param options [Editor options](#TextDocumentShowOptions) to configure the behavior of showing the [editor](#TextEditor). - * @return A promise that resolves to an [editor](#TextEditor). + * @param options {@link TextDocumentShowOptions Editor options} to configure the behavior of showing the {@link TextEditor editor}. + * @return A promise that resolves to an {@link TextEditor editor}. */ export function showTextDocument(document: TextDocument, options?: TextDocumentShowOptions): Thenable; /** * A short-hand for `openTextDocument(uri).then(document => showTextDocument(document, options))`. * - * @see [openTextDocument](#openTextDocument) + * @see {@link openTextDocument} * * @param uri A resource identifier. - * @param options [Editor options](#TextDocumentShowOptions) to configure the behavior of showing the [editor](#TextEditor). - * @return A promise that resolves to an [editor](#TextEditor). + * @param options {@link TextDocumentShowOptions Editor options} to configure the behavior of showing the {@link TextEditor editor}. + * @return A promise that resolves to an {@link TextEditor editor}. */ export function showTextDocument(uri: Uri, options?: TextDocumentShowOptions): Thenable; @@ -8473,7 +8511,7 @@ declare module 'vscode' { /** * Show an information message. * - * @see [showInformationMessage](#window.showInformationMessage) + * @see {@link window.showInformationMessage showInformationMessage} * * @param message The message to show. * @param items A set of items that will be rendered as actions in the message. @@ -8484,7 +8522,7 @@ declare module 'vscode' { /** * Show an information message. * - * @see [showInformationMessage](#window.showInformationMessage) + * @see {@link window.showInformationMessage showInformationMessage} * * @param message The message to show. * @param options Configures the behaviour of the message. @@ -8496,7 +8534,7 @@ declare module 'vscode' { /** * Show a warning message. * - * @see [showInformationMessage](#window.showInformationMessage) + * @see {@link window.showInformationMessage showInformationMessage} * * @param message The message to show. * @param items A set of items that will be rendered as actions in the message. @@ -8507,7 +8545,7 @@ declare module 'vscode' { /** * Show a warning message. * - * @see [showInformationMessage](#window.showInformationMessage) + * @see {@link window.showInformationMessage showInformationMessage} * * @param message The message to show. * @param options Configures the behaviour of the message. @@ -8519,7 +8557,7 @@ declare module 'vscode' { /** * Show a warning message. * - * @see [showInformationMessage](#window.showInformationMessage) + * @see {@link window.showInformationMessage showInformationMessage} * * @param message The message to show. * @param items A set of items that will be rendered as actions in the message. @@ -8530,7 +8568,7 @@ declare module 'vscode' { /** * Show a warning message. * - * @see [showInformationMessage](#window.showInformationMessage) + * @see {@link window.showInformationMessage showInformationMessage} * * @param message The message to show. * @param options Configures the behaviour of the message. @@ -8542,7 +8580,7 @@ declare module 'vscode' { /** * Show an error message. * - * @see [showInformationMessage](#window.showInformationMessage) + * @see {@link window.showInformationMessage showInformationMessage} * * @param message The message to show. * @param items A set of items that will be rendered as actions in the message. @@ -8553,7 +8591,7 @@ declare module 'vscode' { /** * Show an error message. * - * @see [showInformationMessage](#window.showInformationMessage) + * @see {@link window.showInformationMessage showInformationMessage} * * @param message The message to show. * @param options Configures the behaviour of the message. @@ -8565,7 +8603,7 @@ declare module 'vscode' { /** * Show an error message. * - * @see [showInformationMessage](#window.showInformationMessage) + * @see {@link window.showInformationMessage showInformationMessage} * * @param message The message to show. * @param items A set of items that will be rendered as actions in the message. @@ -8576,7 +8614,7 @@ declare module 'vscode' { /** * Show an error message. * - * @see [showInformationMessage](#window.showInformationMessage) + * @see {@link window.showInformationMessage showInformationMessage} * * @param message The message to show. * @param options Configures the behaviour of the message. @@ -8593,7 +8631,7 @@ declare module 'vscode' { * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selected items or `undefined`. */ - export function showQuickPick(items: string[] | Thenable, options: QuickPickOptions & { canPickMany: true; }, token?: CancellationToken): Thenable; + export function showQuickPick(items: readonly string[] | Thenable, options: QuickPickOptions & { canPickMany: true; }, token?: CancellationToken): Thenable; /** * Shows a selection list. @@ -8603,7 +8641,7 @@ declare module 'vscode' { * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selection or `undefined`. */ - export function showQuickPick(items: string[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; + export function showQuickPick(items: readonly string[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; /** * Shows a selection list allowing multiple selections. @@ -8613,7 +8651,7 @@ declare module 'vscode' { * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selected items or `undefined`. */ - export function showQuickPick(items: T[] | Thenable, options: QuickPickOptions & { canPickMany: true; }, token?: CancellationToken): Thenable; + export function showQuickPick(items: readonly T[] | Thenable, options: QuickPickOptions & { canPickMany: true; }, token?: CancellationToken): Thenable; /** * Shows a selection list. @@ -8623,10 +8661,10 @@ declare module 'vscode' { * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selected item or `undefined`. */ - export function showQuickPick(items: T[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; + export function showQuickPick(items: readonly T[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; /** - * Shows a selection list of [workspace folders](#workspace.workspaceFolders) to pick from. + * Shows a selection list of {@link workspace.workspaceFolders workspace folders} to pick from. * Returns `undefined` if no folder is open. * * @param options Configures the behavior of the workspace folder list. @@ -8666,30 +8704,30 @@ declare module 'vscode' { export function showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable; /** - * Creates a [QuickPick](#QuickPick) to let the user pick an item from a list + * Creates a {@link QuickPick} to let the user pick an item from a list * of items of type T. * - * Note that in many cases the more convenient [window.showQuickPick](#window.showQuickPick) - * is easier to use. [window.createQuickPick](#window.createQuickPick) should be used - * when [window.showQuickPick](#window.showQuickPick) does not offer the required flexibility. + * Note that in many cases the more convenient {@link window.showQuickPick} + * is easier to use. {@link window.createQuickPick} should be used + * when {@link window.showQuickPick} does not offer the required flexibility. * - * @return A new [QuickPick](#QuickPick). + * @return A new {@link QuickPick}. */ export function createQuickPick(): QuickPick; /** - * Creates a [InputBox](#InputBox) to let the user enter some text input. + * Creates a {@link InputBox} to let the user enter some text input. * - * Note that in many cases the more convenient [window.showInputBox](#window.showInputBox) - * is easier to use. [window.createInputBox](#window.createInputBox) should be used - * when [window.showInputBox](#window.showInputBox) does not offer the required flexibility. + * Note that in many cases the more convenient {@link window.showInputBox} + * is easier to use. {@link window.createInputBox} should be used + * when {@link window.showInputBox} does not offer the required flexibility. * - * @return A new [InputBox](#InputBox). + * @return A new {@link InputBox}. */ export function createInputBox(): InputBox; /** - * Creates a new [output channel](#OutputChannel) with the given name. + * Creates a new {@link OutputChannel output channel} with the given name. * * @param name Human-readable string which will be used to represent the channel in the UI. */ @@ -8709,9 +8747,9 @@ declare module 'vscode' { /** * Set a message to the status bar. This is a short hand for the more powerful - * status bar [items](#window.createStatusBarItem). + * status bar {@link window.createStatusBarItem items}. * - * @param text The message to show, supports icon substitution as in status bar [items](#StatusBarItem.text). + * @param text The message to show, supports icon substitution as in status bar {@link StatusBarItem.text items}. * @param hideAfterTimeout Timeout in milliseconds after which the message will be disposed. * @return A disposable which hides the status bar message. */ @@ -8719,9 +8757,9 @@ declare module 'vscode' { /** * Set a message to the status bar. This is a short hand for the more powerful - * status bar [items](#window.createStatusBarItem). + * status bar {@link window.createStatusBarItem items}. * - * @param text The message to show, supports icon substitution as in status bar [items](#StatusBarItem.text). + * @param text The message to show, supports icon substitution as in status bar {@link StatusBarItem.text items}. * @param hideWhenDone Thenable on which completion (resolve or reject) the message will be disposed. * @return A disposable which hides the status bar message. */ @@ -8729,12 +8767,12 @@ declare module 'vscode' { /** * Set a message to the status bar. This is a short hand for the more powerful - * status bar [items](#window.createStatusBarItem). + * status bar {@link window.createStatusBarItem items}. * * *Note* that status bar messages stack and that they must be disposed when no * longer used. * - * @param text The message to show, supports icon substitution as in status bar [items](#StatusBarItem.text). + * @param text The message to show, supports icon substitution as in status bar {@link StatusBarItem.text items}. * @return A disposable which hides the status bar message. */ export function setStatusBarMessage(text: string): Disposable; @@ -8746,7 +8784,7 @@ declare module 'vscode' { * @deprecated Use `withProgress` instead. * * @param task A callback returning a promise. Progress increments can be reported with - * the provided [progress](#Progress)-object. + * the provided {@link Progress}-object. * @return The thenable the task did return. */ export function withScmProgress(task: (progress: Progress) => Thenable): Thenable; @@ -8754,17 +8792,17 @@ declare module 'vscode' { /** * Show progress in the editor. Progress is shown while running the given callback * and while the promise it returned isn't resolved nor rejected. The location at which - * progress should show (and other details) is defined via the passed [`ProgressOptions`](#ProgressOptions). + * progress should show (and other details) is defined via the passed {@link ProgressOptions `ProgressOptions`}. * * @param task A callback returning a promise. Progress state can be reported with - * the provided [progress](#Progress)-object. + * the provided {@link Progress}-object. * * To report discrete progress, use `increment` to indicate how much work has been completed. Each call with * a `increment` value will be summed up and reflected as overall progress until 100% is reached (a value of * e.g. `10` accounts for `10%` of work done). * Note that currently only `ProgressLocation.Notification` is capable of showing discrete progress. * - * To monitor if the operation has been cancelled by the user, use the provided [`CancellationToken`](#CancellationToken). + * To monitor if the operation has been cancelled by the user, use the provided {@link CancellationToken `CancellationToken`}. * Note that currently only `ProgressLocation.Notification` is supporting to show a cancel button to cancel the * long running operation. * @@ -8773,7 +8811,7 @@ declare module 'vscode' { export function withProgress(options: ProgressOptions, task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable): Thenable; /** - * Creates a status bar [item](#StatusBarItem). + * Creates a status bar {@link StatusBarItem item}. * * @param alignment The alignment of the item. * @param priority The priority of the item. Higher values mean the item should be shown more to the left. @@ -8782,7 +8820,17 @@ declare module 'vscode' { export function createStatusBarItem(alignment?: StatusBarAlignment, priority?: number): StatusBarItem; /** - * Creates a [Terminal](#Terminal) with a backing shell process. The cwd of the terminal will be the workspace + * Creates a status bar {@link StatusBarItem item}. + * + * @param id The unique identifier of the item. + * @param alignment The alignment of the item. + * @param priority The priority of the item. Higher values mean the item should be shown more to the left. + * @return A new status bar item. + */ + export function createStatusBarItem(id: string, alignment?: StatusBarAlignment, priority?: number): StatusBarItem; + + /** + * Creates a {@link Terminal} with a backing shell process. The cwd of the terminal will be the workspace * directory if it exists. * * @param name Optional human-readable string which will be used to represent the terminal in the UI. @@ -8796,7 +8844,7 @@ declare module 'vscode' { export function createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): Terminal; /** - * Creates a [Terminal](#Terminal) with a backing shell process. + * Creates a {@link Terminal} with a backing shell process. * * @param options A TerminalOptions object describing the characteristics of the new terminal. * @return A new Terminal. @@ -8805,35 +8853,35 @@ declare module 'vscode' { export function createTerminal(options: TerminalOptions): Terminal; /** - * Creates a [Terminal](#Terminal) where an extension controls its input and output. + * Creates a {@link Terminal} where an extension controls its input and output. * - * @param options An [ExtensionTerminalOptions](#ExtensionTerminalOptions) object describing + * @param options An {@link ExtensionTerminalOptions} object describing * the characteristics of the new terminal. * @return A new Terminal. */ export function createTerminal(options: ExtensionTerminalOptions): Terminal; /** - * Register a [TreeDataProvider](#TreeDataProvider) for the view contributed using the extension point `views`. - * This will allow you to contribute data to the [TreeView](#TreeView) and update if the data changes. + * Register a {@link TreeDataProvider} for the view contributed using the extension point `views`. + * This will allow you to contribute data to the {@link TreeView} and update if the data changes. * - * **Note:** To get access to the [TreeView](#TreeView) and perform operations on it, use [createTreeView](#window.createTreeView). + * **Note:** To get access to the {@link TreeView} and perform operations on it, use {@link window.createTreeView createTreeView}. * * @param viewId Id of the view contributed using the extension point `views`. - * @param treeDataProvider A [TreeDataProvider](#TreeDataProvider) that provides tree data for the view + * @param treeDataProvider A {@link TreeDataProvider} that provides tree data for the view */ export function registerTreeDataProvider(viewId: string, treeDataProvider: TreeDataProvider): Disposable; /** - * Create a [TreeView](#TreeView) for the view contributed using the extension point `views`. + * Create a {@link TreeView} for the view contributed using the extension point `views`. * @param viewId Id of the view contributed using the extension point `views`. - * @param options Options for creating the [TreeView](#TreeView) - * @returns a [TreeView](#TreeView). + * @param options Options for creating the {@link TreeView} + * @returns a {@link TreeView}. */ export function createTreeView(viewId: string, options: TreeViewOptions): TreeView; /** - * Registers a [uri handler](#UriHandler) capable of handling system-wide [uris](#Uri). + * Registers a {@link UriHandler uri handler} capable of handling system-wide {@link Uri uris}. * In case there are multiple windows open, the topmost window will handle the uri. * A uri handler is scoped to the extension it is contributed from; it will only * be able to handle uris which are directed to the extension itself. A uri must respect @@ -8859,7 +8907,7 @@ declare module 'vscode' { * Registers a webview panel serializer. * * Extensions that support reviving should have an `"onWebviewPanel:viewType"` activation event and - * make sure that [registerWebviewPanelSerializer](#registerWebviewPanelSerializer) is called during activation. + * make sure that {@link registerWebviewPanelSerializer} is called during activation. * * Only a single serializer may be registered at a time for a given `viewType`. * @@ -8888,7 +8936,7 @@ declare module 'vscode' { * * Normally the webview's html context is created when the view becomes visible * and destroyed when it is hidden. Extensions that have complex state - * or UI can set the `retainContextWhenHidden` to make VS Code keep the webview + * or UI can set the `retainContextWhenHidden` to make the editor keep the webview * context around, even when the webview moves to a background tab. When a webview using * `retainContextWhenHidden` becomes hidden, its scripts and other dynamic content are suspended. * When the view becomes visible again, the context is automatically restored @@ -8905,9 +8953,9 @@ declare module 'vscode' { /** * Register a provider for custom editors for the `viewType` contributed by the `customEditors` extension point. * - * When a custom editor is opened, VS Code fires an `onCustomEditor:viewType` activation event. Your extension - * must register a [`CustomTextEditorProvider`](#CustomTextEditorProvider), [`CustomReadonlyEditorProvider`](#CustomReadonlyEditorProvider), - * [`CustomEditorProvider`](#CustomEditorProvider)for `viewType` as part of activation. + * When a custom editor is opened, an `onCustomEditor:viewType` activation event is fired. Your extension + * must register a {@link CustomTextEditorProvider `CustomTextEditorProvider`}, {@link CustomReadonlyEditorProvider `CustomReadonlyEditorProvider`}, + * {@link CustomEditorProvider `CustomEditorProvider`}for `viewType` as part of activation. * * @param viewType Unique identifier for the custom editor provider. This should match the `viewType` from the * `customEditors` contribution point. @@ -8928,7 +8976,7 @@ declare module 'vscode' { * Indicates that the provider allows multiple editor instances to be open at the same time for * the same resource. * - * By default, VS Code only allows one editor instance to be open at a time for each resource. If the + * By default, the editor only allows one editor instance to be open at a time for each resource. If the * user tries to open a second editor instance for the resource, the first one is instead moved to where * the second one was to be opened. * @@ -8949,8 +8997,8 @@ declare module 'vscode' { /** * Register a file decoration provider. * - * @param provider A [FileDecorationProvider](#FileDecorationProvider). - * @return A [disposable](#Disposable) that unregisters the provider. + * @param provider A {@link FileDecorationProvider}. + * @return A {@link Disposable} that unregisters the provider. */ export function registerFileDecorationProvider(provider: FileDecorationProvider): Disposable; @@ -8961,13 +9009,13 @@ declare module 'vscode' { export let activeColorTheme: ColorTheme; /** - * An [event](#Event) which fires when the active color theme is changed or has changes. + * An {@link Event} which fires when the active color theme is changed or has changes. */ export const onDidChangeActiveColorTheme: Event; } /** - * Options for creating a [TreeView](#TreeView) + * Options for creating a {@link TreeView} */ export interface TreeViewOptions { @@ -8990,7 +9038,7 @@ declare module 'vscode' { } /** - * The event that is fired when an element in the [TreeView](#TreeView) is expanded or collapsed + * The event that is fired when an element in the {@link TreeView} is expanded or collapsed */ export interface TreeViewExpansionEvent { @@ -9002,7 +9050,7 @@ declare module 'vscode' { } /** - * The event that is fired when there is a change in [tree view's selection](#TreeView.selection) + * The event that is fired when there is a change in {@link TreeView.selection tree view's selection} */ export interface TreeViewSelectionChangeEvent { @@ -9014,12 +9062,12 @@ declare module 'vscode' { } /** - * The event that is fired when there is a change in [tree view's visibility](#TreeView.visible) + * The event that is fired when there is a change in {@link TreeView.visible tree view's visibility} */ export interface TreeViewVisibilityChangeEvent { /** - * `true` if the [tree view](#TreeView) is visible otherwise `false`. + * `true` if the {@link TreeView tree view} is visible otherwise `false`. */ readonly visible: boolean; @@ -9046,17 +9094,17 @@ declare module 'vscode' { readonly selection: T[]; /** - * Event that is fired when the [selection](#TreeView.selection) has changed + * Event that is fired when the {@link TreeView.selection selection} has changed */ readonly onDidChangeSelection: Event>; /** - * `true` if the [tree view](#TreeView) is visible otherwise `false`. + * `true` if the {@link TreeView tree view} is visible otherwise `false`. */ readonly visible: boolean; /** - * Event that is fired when [visibility](#TreeView.visible) has changed + * Event that is fired when {@link TreeView.visible visibility} has changed */ readonly onDidChangeVisibility: Event; @@ -9088,7 +9136,7 @@ declare module 'vscode' { * In order to expand the revealed element, set the option `expand` to `true`. To expand recursively set `expand` to the number of levels to expand. * **NOTE:** You can expand only to 3 levels maximum. * - * **NOTE:** The [TreeDataProvider](#TreeDataProvider) that the `TreeView` [is registered with](#window.createTreeView) with must implement [getParent](#TreeDataProvider.getParent) method to access this API. + * **NOTE:** The {@link TreeDataProvider} that the `TreeView` {@link window.createTreeView is registered with} with must implement {@link TreeDataProvider.getParent getParent} method to access this API. */ reveal(element: T, options?: { select?: boolean, focus?: boolean, expand?: boolean | number }): Thenable; } @@ -9105,10 +9153,10 @@ declare module 'vscode' { onDidChangeTreeData?: Event; /** - * Get [TreeItem](#TreeItem) representation of the `element` + * Get {@link TreeItem} representation of the `element` * - * @param element The element for which [TreeItem](#TreeItem) representation is asked for. - * @return [TreeItem](#TreeItem) representation of the element + * @param element The element for which {@link TreeItem} representation is asked for. + * @return {@link TreeItem} representation of the element */ getTreeItem(element: T): TreeItem | Thenable; @@ -9124,7 +9172,7 @@ declare module 'vscode' { * Optional method to return the parent of `element`. * Return `null` or `undefined` if `element` is a child of root. * - * **NOTE:** This method should be implemented in order to access [reveal](#TreeView.reveal) API. + * **NOTE:** This method should be implemented in order to access {@link TreeView.reveal reveal} API. * * @param element The element for which the parent has to be returned. * @return Parent of `element`. @@ -9132,8 +9180,8 @@ declare module 'vscode' { getParent?(element: T): ProviderResult; /** - * 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. + * Called on hover to resolve the {@link TreeItem.tooltip TreeItem} property if it is undefined. + * Called on tree item click/open to resolve the {@link TreeItem.command TreeItem} 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. @@ -9157,7 +9205,7 @@ declare module 'vscode' { export class TreeItem { /** - * A human-readable string describing this item. When `falsy`, it is derived from [resourceUri](#TreeItem.resourceUri). + * A human-readable string describing this item. When `falsy`, it is derived from {@link TreeItem.resourceUri resourceUri}. */ label?: string | TreeItemLabel; @@ -9169,23 +9217,23 @@ declare module 'vscode' { id?: string; /** - * The icon path or [ThemeIcon](#ThemeIcon) for the tree item. - * When `falsy`, [Folder Theme Icon](#ThemeIcon.Folder) is assigned, if item is collapsible otherwise [File Theme Icon](#ThemeIcon.File). - * When a file or folder [ThemeIcon](#ThemeIcon) is specified, icon is derived from the current file icon theme for the specified theme icon using [resourceUri](#TreeItem.resourceUri) (if provided). + * The icon path or {@link ThemeIcon} for the tree item. + * When `falsy`, {@link ThemeIcon.Folder Folder Theme Icon} is assigned, if item is collapsible otherwise {@link ThemeIcon.File File Theme Icon}. + * When a file or folder {@link ThemeIcon} is specified, icon is derived from the current file icon theme for the specified theme icon using {@link TreeItem.resourceUri resourceUri} (if provided). */ iconPath?: string | Uri | { light: string | Uri; dark: string | Uri } | ThemeIcon; /** * A human-readable string which is rendered less prominent. - * When `true`, it is derived from [resourceUri](#TreeItem.resourceUri) and when `falsy`, it is not shown. + * When `true`, it is derived from {@link TreeItem.resourceUri resourceUri} and when `falsy`, it is not shown. */ description?: string | boolean; /** - * The [uri](#Uri) of the resource representing this item. + * The {@link Uri} of the resource representing this item. * - * Will be used to derive the [label](#TreeItem.label), when it is not provided. - * Will be used to derive the icon from current file icon theme, when [iconPath](#TreeItem.iconPath) has [ThemeIcon](#ThemeIcon) value. + * Will be used to derive the {@link TreeItem.label label}, when it is not provided. + * Will be used to derive the icon from current file icon theme, when {@link TreeItem.iconPath iconPath} has {@link ThemeIcon} value. */ resourceUri?: Uri; @@ -9195,7 +9243,7 @@ declare module 'vscode' { tooltip?: string | MarkdownString | undefined; /** - * The [command](#Command) that should be executed when the tree item is selected. + * The {@link Command} that should be executed when the tree item is selected. * * Please use `vscode.open` or `vscode.diff` as command IDs when the tree item is opening * something in the editor. Using these commands ensures that the resulting editor will @@ -9204,7 +9252,7 @@ declare module 'vscode' { command?: Command; /** - * [TreeItemCollapsibleState](#TreeItemCollapsibleState) of the tree item. + * {@link TreeItemCollapsibleState} of the tree item. */ collapsibleState?: TreeItemCollapsibleState; @@ -9237,13 +9285,13 @@ declare module 'vscode' { /** * @param label A human-readable string describing this item - * @param collapsibleState [TreeItemCollapsibleState](#TreeItemCollapsibleState) of the tree item. Default is [TreeItemCollapsibleState.None](#TreeItemCollapsibleState.None) + * @param collapsibleState {@link TreeItemCollapsibleState} of the tree item. Default is {@link TreeItemCollapsibleState.None} */ constructor(label: string | TreeItemLabel, collapsibleState?: TreeItemCollapsibleState); /** - * @param resourceUri The [uri](#Uri) of the resource representing this item. - * @param collapsibleState [TreeItemCollapsibleState](#TreeItemCollapsibleState) of the tree item. Default is [TreeItemCollapsibleState.None](#TreeItemCollapsibleState.None) + * @param resourceUri The {@link Uri} of the resource representing this item. + * @param collapsibleState {@link TreeItemCollapsibleState} of the tree item. Default is {@link TreeItemCollapsibleState.None} */ constructor(resourceUri: Uri, collapsibleState?: TreeItemCollapsibleState); } @@ -9267,12 +9315,12 @@ declare module 'vscode' { } /** - * Label describing the [Tree item](#TreeItem) + * Label describing the {@link TreeItem Tree item} */ export interface TreeItemLabel { /** - * A human-readable string describing the [Tree item](#TreeItem). + * A human-readable string describing the {@link TreeItem Tree item}. */ label: string; @@ -9309,7 +9357,7 @@ declare module 'vscode' { cwd?: string | Uri; /** - * Object with environment variables that will be added to the VS Code process. + * Object with environment variables that will be added to the editor process. */ env?: { [key: string]: string | null | undefined }; @@ -9330,6 +9378,13 @@ declare module 'vscode' { * as normal. */ hideFromUser?: boolean; + + /** + * A message to write to the terminal on first launch, note that this is not sent to the + * process but, rather written directly to the terminal. This supports escape sequences such + * a setting text style. + */ + message?: string; } /** @@ -9342,7 +9397,7 @@ declare module 'vscode' { name: string; /** - * An implementation of [Pseudoterminal](#Pseudoterminal) that allows an extension to + * An implementation of {@link Pseudoterminal} that allows an extension to * control a terminal. */ pty: Pseudoterminal; @@ -9354,7 +9409,7 @@ declare module 'vscode' { interface Pseudoterminal { /** * An event that when fired will write data to the terminal. Unlike - * [Terminal.sendText](#Terminal.sendText) which sends text to the underlying child + * {@link Terminal.sendText} which sends text to the underlying child * pseudo-device (the child), this will write the text to parent pseudo-device (the * _terminal_ itself). * @@ -9380,7 +9435,7 @@ declare module 'vscode' { onDidWrite: Event; /** - * An event that when fired allows overriding the [dimensions](#Pseudoterminal.setDimensions) of the + * An event that when fired allows overriding the {@link Pseudoterminal.setDimensions dimensions} of the * terminal. Note that when set, the overridden dimensions will only take effect when they * are lower than the actual dimensions of the terminal (ie. there will never be a scroll * bar). Set to `undefined` for the terminal to go back to the regular dimensions (fit to @@ -9449,7 +9504,7 @@ declare module 'vscode' { /** * Implement to handle incoming keystrokes in the terminal or when an extension calls - * [Terminal.sendText](#Terminal.sendText). `data` contains the keystrokes/text serialized into + * {@link Terminal.sendText}. `data` contains the keystrokes/text serialized into * their corresponding VT sequence representation. * * @param data The incoming data. @@ -9476,7 +9531,7 @@ declare module 'vscode' { * 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 + * {@link Pseudoterminal.onDidOverrideDimensions onDidOverrideDimensions}, `setDimensions` will * continue to be called with the regular panel dimensions, allowing the extension continue * to react dimension changes. * @@ -9672,23 +9727,23 @@ declare module 'vscode' { /** * A light-weight user input UI that is initially not visible. After * configuring it through its properties the extension can make it - * visible by calling [QuickInput.show](#QuickInput.show). + * visible by calling {@link QuickInput.show}. * * There are several reasons why this UI might have to be hidden and - * the extension will be notified through [QuickInput.onDidHide](#QuickInput.onDidHide). - * (Examples include: an explicit call to [QuickInput.hide](#QuickInput.hide), + * the extension will be notified through {@link QuickInput.onDidHide}. + * (Examples include: an explicit call to {@link QuickInput.hide}, * the user pressing Esc, some other input UI opening, etc.) * * A user pressing Enter or some other gesture implying acceptance * of the current state does not automatically hide this UI component. * It is up to the extension to decide whether to accept the user's input - * and if the UI should indeed be hidden through a call to [QuickInput.hide](#QuickInput.hide). + * and if the UI should indeed be hidden through a call to {@link QuickInput.hide}. * * When the extension no longer needs this input UI, it should - * [QuickInput.dispose](#QuickInput.dispose) it to allow for freeing up + * {@link QuickInput.dispose} it to allow for freeing up * any resources associated with it. * - * See [QuickPick](#QuickPick) and [InputBox](#InputBox) for concrete UIs. + * See {@link QuickPick} and {@link InputBox} for concrete UIs. */ export interface QuickInput { @@ -9730,12 +9785,12 @@ declare module 'vscode' { /** * Makes the input UI visible in its current configuration. Any other input - * UI will first fire an [QuickInput.onDidHide](#QuickInput.onDidHide) event. + * UI will first fire an {@link QuickInput.onDidHide} event. */ show(): void; /** - * Hides this input UI. This will also fire an [QuickInput.onDidHide](#QuickInput.onDidHide) + * Hides this input UI. This will also fire an {@link QuickInput.onDidHide} * event. */ hide(): void; @@ -9744,8 +9799,8 @@ declare module 'vscode' { * An event signaling when this input UI is hidden. * * There are several reasons why this UI might have to be hidden and - * the extension will be notified through [QuickInput.onDidHide](#QuickInput.onDidHide). - * (Examples include: an explicit call to [QuickInput.hide](#QuickInput.hide), + * the extension will be notified through {@link QuickInput.onDidHide}. + * (Examples include: an explicit call to {@link QuickInput.hide}, * the user pressing Esc, some other input UI opening, etc.) */ onDidHide: Event; @@ -9760,14 +9815,14 @@ declare module 'vscode' { } /** - * A concrete [QuickInput](#QuickInput) to let the user pick an item from a + * A concrete {@link QuickInput} to let the user pick an item from a * list of items of type T. The items can be filtered through a filter text field and - * there is an option [canSelectMany](#QuickPick.canSelectMany) to allow for + * there is an option {@link QuickPick.canSelectMany canSelectMany} to allow for * selecting multiple items. * - * Note that in many cases the more convenient [window.showQuickPick](#window.showQuickPick) - * is easier to use. [window.createQuickPick](#window.createQuickPick) should be used - * when [window.showQuickPick](#window.showQuickPick) does not offer the required flexibility. + * Note that in many cases the more convenient {@link window.showQuickPick} + * is easier to use. {@link window.createQuickPick} should be used + * when {@link window.showQuickPick} does not offer the required flexibility. */ export interface QuickPick extends QuickInput { @@ -9794,7 +9849,7 @@ declare module 'vscode' { /** * Buttons for actions in the UI. */ - buttons: ReadonlyArray; + buttons: readonly QuickInputButton[]; /** * An event signaling when a button was triggered. @@ -9802,9 +9857,9 @@ declare module 'vscode' { readonly onDidTriggerButton: Event; /** - * Items to pick from. + * Items to pick from. This can be read and updated by the extension. */ - items: ReadonlyArray; + items: readonly T[]; /** * If multiple items can be selected at the same time. Defaults to false. @@ -9824,7 +9879,7 @@ declare module 'vscode' { /** * Active items. This can be read and updated by the extension. */ - activeItems: ReadonlyArray; + activeItems: readonly T[]; /** * An event signaling when the active items have changed. @@ -9834,7 +9889,7 @@ declare module 'vscode' { /** * Selected items. This can be read and updated by the extension. */ - selectedItems: ReadonlyArray; + selectedItems: readonly T[]; /** * An event signaling when the selected items have changed. @@ -9843,11 +9898,11 @@ declare module 'vscode' { } /** - * A concrete [QuickInput](#QuickInput) to let the user input a text value. + * A concrete {@link QuickInput} to let the user input a text value. * - * Note that in many cases the more convenient [window.showInputBox](#window.showInputBox) - * is easier to use. [window.createInputBox](#window.createInputBox) should be used - * when [window.showInputBox](#window.showInputBox) does not offer the required flexibility. + * Note that in many cases the more convenient {@link window.showInputBox} + * is easier to use. {@link window.createInputBox} should be used + * when {@link window.showInputBox} does not offer the required flexibility. */ export interface InputBox extends QuickInput { @@ -9879,7 +9934,7 @@ declare module 'vscode' { /** * Buttons for actions in the UI. */ - buttons: ReadonlyArray; + buttons: readonly QuickInputButton[]; /** * An event signaling when a button was triggered. @@ -9898,7 +9953,7 @@ declare module 'vscode' { } /** - * Button for an action in a [QuickPick](#QuickPick) or [InputBox](#InputBox). + * Button for an action in a {@link QuickPick} or {@link InputBox}. */ export interface QuickInputButton { @@ -9914,12 +9969,12 @@ declare module 'vscode' { } /** - * Predefined buttons for [QuickPick](#QuickPick) and [InputBox](#InputBox). + * Predefined buttons for {@link QuickPick} and {@link InputBox}. */ export class QuickInputButtons { /** - * A back button for [QuickPick](#QuickPick) and [InputBox](#InputBox). + * A back button for {@link QuickPick} and {@link InputBox}. * * When a navigation 'back' button is needed this one should be used for consistency. * It comes with a predefined icon, tooltip and location. @@ -9933,7 +9988,7 @@ declare module 'vscode' { } /** - * An event describing an individual change in the text of a [document](#TextDocument). + * An event describing an individual change in the text of a {@link TextDocument document}. */ export interface TextDocumentContentChangeEvent { /** @@ -9955,7 +10010,7 @@ declare module 'vscode' { } /** - * An event describing a transactional [document](#TextDocument) change. + * An event describing a transactional {@link TextDocument document} change. */ export interface TextDocumentChangeEvent { @@ -9967,7 +10022,7 @@ declare module 'vscode' { /** * An array of content changes. */ - readonly contentChanges: ReadonlyArray; + readonly contentChanges: readonly TextDocumentContentChangeEvent[]; } /** @@ -9993,11 +10048,11 @@ declare module 'vscode' { } /** - * An event that is fired when a [document](#TextDocument) will be saved. + * An event that is fired when a {@link TextDocument document} will be saved. * * To make modifications to the document before it is being saved, call the - * [`waitUntil`](#TextDocumentWillSaveEvent.waitUntil)-function with a thenable - * that resolves to an array of [text edits](#TextEdit). + * {@link TextDocumentWillSaveEvent.waitUntil `waitUntil`}-function with a thenable + * that resolves to an array of {@link TextEdit text edits}. */ export interface TextDocumentWillSaveEvent { @@ -10012,7 +10067,7 @@ declare module 'vscode' { readonly reason: TextDocumentSaveReason; /** - * Allows to pause the event loop and to apply [pre-save-edits](#TextEdit). + * Allows to pause the event loop and to apply {@link TextEdit pre-save-edits}. * Edits of subsequent calls to this function will be applied in order. The * edits will be *ignored* if concurrent modifications of the document happened. * @@ -10029,7 +10084,7 @@ declare module 'vscode' { * }) * ``` * - * @param thenable A thenable that resolves to [pre-save-edits](#TextEdit). + * @param thenable A thenable that resolves to {@link TextEdit pre-save-edits}. */ waitUntil(thenable: Thenable): void; @@ -10047,18 +10102,18 @@ declare module 'vscode' { * An event that is fired when files are going to be created. * * To make modifications to the workspace before the files are created, - * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a - * thenable that resolves to a [workspace edit](#WorkspaceEdit). + * call the {@link FileWillCreateEvent.waitUntil `waitUntil`}-function with a + * thenable that resolves to a {@link WorkspaceEdit workspace edit}. */ export interface FileWillCreateEvent { /** * The files that are going to be created. */ - readonly files: ReadonlyArray; + readonly files: readonly Uri[]; /** - * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). + * Allows to pause the event and to apply a {@link WorkspaceEdit workspace edit}. * * *Note:* This function can only be called during event dispatch and not * in an asynchronous manner: @@ -10095,25 +10150,25 @@ declare module 'vscode' { /** * The files that got created. */ - readonly files: ReadonlyArray; + readonly files: readonly Uri[]; } /** * An event that is fired when files are going to be deleted. * * To make modifications to the workspace before the files are deleted, - * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a - * thenable that resolves to a [workspace edit](#WorkspaceEdit). + * call the {@link FileWillCreateEvent.waitUntil `waitUntil}-function with a + * thenable that resolves to a {@link WorkspaceEdit workspace edit}. */ export interface FileWillDeleteEvent { /** * The files that are going to be deleted. */ - readonly files: ReadonlyArray; + readonly files: readonly Uri[]; /** - * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). + * Allows to pause the event and to apply a {@link WorkspaceEdit workspace edit}. * * *Note:* This function can only be called during event dispatch and not * in an asynchronous manner: @@ -10150,25 +10205,25 @@ declare module 'vscode' { /** * The files that got deleted. */ - readonly files: ReadonlyArray; + readonly files: readonly Uri[]; } /** * An event that is fired when files are going to be renamed. * * To make modifications to the workspace before the files are renamed, - * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a - * thenable that resolves to a [workspace edit](#WorkspaceEdit). + * call the {@link FileWillCreateEvent.waitUntil `waitUntil}-function with a + * thenable that resolves to a {@link WorkspaceEdit workspace edit}. */ export interface FileWillRenameEvent { /** * The files that are going to be renamed. */ - readonly files: ReadonlyArray<{ oldUri: Uri, newUri: Uri }>; + readonly files: ReadonlyArray<{ readonly oldUri: Uri, readonly newUri: Uri }>; /** - * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). + * Allows to pause the event and to apply a {@link WorkspaceEdit workspace edit}. * * *Note:* This function can only be called during event dispatch and not * in an asynchronous manner: @@ -10205,22 +10260,22 @@ declare module 'vscode' { /** * The files that got renamed. */ - readonly files: ReadonlyArray<{ oldUri: Uri, newUri: Uri }>; + readonly files: ReadonlyArray<{ readonly oldUri: Uri, readonly newUri: Uri }>; } /** - * An event describing a change to the set of [workspace folders](#workspace.workspaceFolders). + * An event describing a change to the set of {@link workspace.workspaceFolders workspace folders}. */ export interface WorkspaceFoldersChangeEvent { /** * Added workspace folders. */ - readonly added: ReadonlyArray; + readonly added: readonly WorkspaceFolder[]; /** * Removed workspace folders. */ - readonly removed: ReadonlyArray; + readonly removed: readonly WorkspaceFolder[]; } /** @@ -10232,14 +10287,14 @@ declare module 'vscode' { /** * The associated uri for this workspace folder. * - * *Note:* The [Uri](#Uri)-type was intentionally chosen such that future releases of the editor can support + * *Note:* The {@link Uri}-type was intentionally chosen such that future releases of the editor can support * workspace folders that are not stored on the local disk, e.g. `ftp://server/workspaces/foo`. */ readonly uri: Uri; /** * The name of this workspace folder. Defaults to - * the basename of its [uri-path](#Uri.path) + * the basename of its {@link Uri.path uri-path} */ readonly name: string; @@ -10251,24 +10306,24 @@ declare module 'vscode' { /** * Namespace for dealing with the current workspace. A workspace is the collection of one - * or more folders that are opened in a VS Code window (instance). + * or more folders that are opened in an editor window (instance). * - * It is also possible to open VS Code without a workspace. For example, when you open a - * new VS Code window by selecting a file from your platform's File menu, you will not be - * inside a workspace. In this mode, some of VS Code's capabilities are reduced but you can + * It is also possible to open an editor without a workspace. For example, when you open a + * new editor window by selecting a file from your platform's File menu, you will not be + * inside a workspace. In this mode, some of the editor's capabilities are reduced but you can * still open text files and edit them. * * Refer to https://code.visualstudio.com/docs/editor/workspaces for more information on - * the concept of workspaces in VS Code. + * the concept of workspaces. * - * The workspace offers support for [listening](#workspace.createFileSystemWatcher) to fs - * events and for [finding](#workspace.findFiles) files. Both perform well and run _outside_ + * The workspace offers support for {@link workspace.createFileSystemWatcher listening} to fs + * events and for {@link workspace.findFiles finding} files. Both perform well and run _outside_ * the editor-process so that they should be always used instead of nodejs-equivalents. */ export namespace workspace { /** - * A [file system](#FileSystem) instance that allows to interact with local and remote + * A {@link FileSystem file system} instance that allows to interact with local and remote * files, e.g. `vscode.workspace.fs.readDirectory(someUri)` allows to retrieve all entries * of a directory or `vscode.workspace.fs.stat(anotherUri)` returns the meta data for a * file. @@ -10276,33 +10331,33 @@ declare module 'vscode' { export const fs: FileSystem; /** - * The workspace folder that is open in VS Code. `undefined` when no workspace + * The workspace folder that is open in the editor. `undefined` when no workspace * has been opened. * * Refer to https://code.visualstudio.com/docs/editor/workspaces for more information - * on workspaces in VS Code. + * on workspaces. * - * @deprecated Use [`workspaceFolders`](#workspace.workspaceFolders) instead. + * @deprecated Use {@link workspace.workspaceFolders `workspaceFolders`} instead. */ export const rootPath: string | undefined; /** - * List of workspace folders that are open in VS Code. `undefined when no workspace + * List of workspace folders that are open in the editor. `undefined` when no workspace * has been opened. * * Refer to https://code.visualstudio.com/docs/editor/workspaces for more information - * on workspaces in VS Code. + * on workspaces. * * *Note* that the first entry corresponds to the value of `rootPath`. */ - export const workspaceFolders: ReadonlyArray | undefined; + export const workspaceFolders: readonly WorkspaceFolder[] | undefined; /** * The name of the workspace. `undefined` when no workspace * has been opened. * * Refer to https://code.visualstudio.com/docs/editor/workspaces for more information on - * the concept of workspaces in VS Code. + * the concept of workspaces. */ export const name: string | undefined; @@ -10331,7 +10386,7 @@ declare module 'vscode' { * ``` * * Refer to https://code.visualstudio.com/docs/editor/workspaces for more information on - * the concept of workspaces in VS Code. + * the concept of workspaces. * * **Note:** it is not advised to use `workspace.workspaceFile` to write * configuration data into the file. You can use `workspace.getConfiguration().update()` @@ -10346,7 +10401,7 @@ declare module 'vscode' { export const onDidChangeWorkspaceFolders: Event; /** - * Returns the [workspace folder](#WorkspaceFolder) that contains a given uri. + * Returns the {@link WorkspaceFolder workspace folder} that contains a given uri. * * returns `undefined` when the given uri doesn't match any workspace folder * * returns the *input* when the given uri is a workspace folder itself * @@ -10358,10 +10413,10 @@ declare module 'vscode' { /** * Returns a path that is relative to the workspace folder or folders. * - * When there are no [workspace folders](#workspace.workspaceFolders) or when the path + * When there are no {@link workspace.workspaceFolders workspace folders} or when the path * is not contained in them, the input is returned. * - * @param pathOrUri A path or uri. When a uri is given its [fsPath](#Uri.fsPath) is used. + * @param pathOrUri A path or uri. When a uri is given its {@link Uri.fsPath fsPath} is used. * @param includeWorkspaceFolder When `true` and when the given path is contained inside a * workspace folder the name of the workspace is prepended. Defaults to `true` when there are * multiple workspace folders and `false` otherwise. @@ -10370,7 +10425,7 @@ declare module 'vscode' { export function asRelativePath(pathOrUri: string | Uri, includeWorkspaceFolder?: boolean): string; /** - * This method replaces `deleteCount` [workspace folders](#workspace.workspaceFolders) starting at index `start` + * This method replaces `deleteCount` {@link workspace.workspaceFolders workspace folders} starting at index `start` * by an optional set of `workspaceFoldersToAdd` on the `vscode.workspace.workspaceFolders` array. This "splice" * behavior can be used to add, remove and change workspace folders in a single operation. * @@ -10378,7 +10433,7 @@ declare module 'vscode' { * one that called this method) will be terminated and restarted so that the (deprecated) `rootPath` property is * updated to point to the first workspace folder. * - * Use the [`onDidChangeWorkspaceFolders()`](#onDidChangeWorkspaceFolders) event to get notified when the + * Use the {@link onDidChangeWorkspaceFolders `onDidChangeWorkspaceFolders()`} event to get notified when the * workspace folders have been updated. * * **Example:** adding a new workspace folder at the end of workspace folders @@ -10399,10 +10454,10 @@ declare module 'vscode' { * It is valid to remove an existing workspace folder and add it again with a different name * to rename that folder. * - * **Note:** it is not valid to call [updateWorkspaceFolders()](#updateWorkspaceFolders) multiple times - * without waiting for the [`onDidChangeWorkspaceFolders()`](#onDidChangeWorkspaceFolders) to fire. + * **Note:** it is not valid to call {@link updateWorkspaceFolders updateWorkspaceFolders()} multiple times + * without waiting for the {@link onDidChangeWorkspaceFolders `onDidChangeWorkspaceFolders()`} to fire. * - * @param start the zero-based location in the list of currently opened [workspace folders](#WorkspaceFolder) + * @param start the zero-based location in the list of currently opened {@link WorkspaceFolder workspace folders} * from which to start deleting workspace folders. * @param deleteCount the optional number of workspace folders to remove. * @param workspaceFoldersToAdd the optional variable set of workspace folders to add in place of the deleted ones. @@ -10418,12 +10473,12 @@ declare module 'vscode' { * A glob pattern that filters the file events on their absolute path must be provided. Optionally, * flags to ignore certain kinds of events can be provided. To stop listening to events the watcher must be disposed. * - * *Note* that only files within the current [workspace folders](#workspace.workspaceFolders) can be watched. + * *Note* that only files within the current {@link workspace.workspaceFolders workspace folders} can be watched. * *Note* that when watching for file changes such as '**​/*.js', notifications will not be sent when a parent folder is * moved or deleted (this is a known limitation of the current implementation and may change in the future). * - * @param globPattern A [glob pattern](#GlobPattern) that is applied to the absolute paths of created, changed, - * and deleted files. Use a [relative pattern](#RelativePattern) to limit events to a certain [workspace folder](#WorkspaceFolder). + * @param globPattern A {@link GlobPattern glob pattern} that is applied to the absolute paths of created, changed, + * and deleted files. Use a {@link RelativePattern relative pattern} to limit events to a certain {@link WorkspaceFolder workspace folder}. * @param ignoreCreateEvents Ignore when files have been created. * @param ignoreChangeEvents Ignore when files have been changed. * @param ignoreDeleteEvents Ignore when files have been deleted. @@ -10432,21 +10487,21 @@ declare module 'vscode' { export function createFileSystemWatcher(globPattern: GlobPattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): FileSystemWatcher; /** - * Find files across all [workspace folders](#workspace.workspaceFolders) in the workspace. + * Find files across all {@link workspace.workspaceFolders workspace folders} in the workspace. * * @example * findFiles('**​/*.js', '**​/node_modules/**', 10) * - * @param include A [glob pattern](#GlobPattern) that defines the files to search for. The glob pattern - * will be matched against the file paths of resulting matches relative to their workspace. Use a [relative pattern](#RelativePattern) - * to restrict the search results to a [workspace folder](#WorkspaceFolder). - * @param exclude A [glob pattern](#GlobPattern) that defines files and folders to exclude. The glob pattern - * will be matched against the file paths of resulting matches relative to their workspace. When `undefined` only default excludes will - * apply, when `null` no excludes will apply. + * @param include A {@link GlobPattern glob pattern} that defines the files to search for. The glob pattern + * will be matched against the file paths of resulting matches relative to their workspace. Use a {@link RelativePattern relative pattern} + * to restrict the search results to a {@link WorkspaceFolder workspace folder}. + * @param exclude A {@link GlobPattern glob pattern} that defines files and folders to exclude. The glob pattern + * will be matched against the file paths of resulting matches relative to their workspace. When `undefined`, default excludes and the user's + * configured excludes will apply. When `null`, no excludes will apply. * @param maxResults An upper-bound for the result. * @param token A token that can be used to signal cancellation to the underlying search engine. * @return A thenable that resolves to an array of resource identifiers. Will return no results if no - * [workspace folders](#workspace.workspaceFolders) are opened. + * {@link workspace.workspaceFolders workspace folders} are opened. */ export function findFiles(include: GlobPattern, exclude?: GlobPattern | null, maxResults?: number, token?: CancellationToken): Thenable; @@ -10460,7 +10515,7 @@ declare module 'vscode' { /** * Make changes to one or many resources or create, delete, and rename resources as defined by the given - * [workspace edit](#WorkspaceEdit). + * {@link WorkspaceEdit workspace edit}. * * All changes of a workspace edit are applied in the same order in which they have been added. If * multiple textual inserts are made at the same position, these strings appear in the resulting text @@ -10477,36 +10532,36 @@ declare module 'vscode' { export function applyEdit(edit: WorkspaceEdit): Thenable; /** - * All text documents currently known to the system. + * All text documents currently known to the editor. */ - export const textDocuments: ReadonlyArray; + export const textDocuments: readonly TextDocument[]; /** * Opens a document. Will return early if this document is already open. Otherwise - * the document is loaded and the [didOpen](#workspace.onDidOpenTextDocument)-event fires. + * the document is loaded and the {@link workspace.onDidOpenTextDocument didOpen}-event fires. * - * The document is denoted by an [uri](#Uri). Depending on the [scheme](#Uri.scheme) the + * The document is denoted by an {@link Uri}. Depending on the {@link Uri.scheme scheme} the * following rules apply: * * `file`-scheme: Open a file on disk, will be rejected if the file does not exist or cannot be loaded. * * `untitled`-scheme: A new file that should be saved on disk, e.g. `untitled:c:\frodo\new.js`. The language * will be derived from the file name. - * * For all other schemes contributed [text document content providers](#TextDocumentContentProvider) and - * [file system providers](#FileSystemProvider) are consulted. + * * For all other schemes contributed {@link TextDocumentContentProvider text document content providers} and + * {@link FileSystemProvider file system providers} are consulted. * * *Note* that the lifecycle of the returned document is owned by the editor and not by the extension. That means an - * [`onDidClose`](#workspace.onDidCloseTextDocument)-event can occur at any time after opening it. + * {@link workspace.onDidCloseTextDocument `onDidClose`}-event can occur at any time after opening it. * * @param uri Identifies the resource to open. - * @return A promise that resolves to a [document](#TextDocument). + * @return A promise that resolves to a {@link TextDocument document}. */ export function openTextDocument(uri: Uri): Thenable; /** * A short-hand for `openTextDocument(Uri.file(fileName))`. * - * @see [openTextDocument](#openTextDocument) + * @see {@link openTextDocument} * @param fileName A name of a file on disk. - * @return A promise that resolves to a [document](#TextDocument). + * @return A promise that resolves to a {@link TextDocument document}. */ export function openTextDocument(fileName: string): Thenable; @@ -10516,7 +10571,7 @@ declare module 'vscode' { * specify the *language* and/or the *content* of the document. * * @param options Options to control how the document will be created. - * @return A promise that resolves to a [document](#TextDocument). + * @return A promise that resolves to a {@link TextDocument document}. */ export function openTextDocument(options?: { language?: string; content?: string; }): Thenable; @@ -10527,30 +10582,30 @@ declare module 'vscode' { * * @param scheme The uri-scheme to register for. * @param provider A content provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerTextDocumentContentProvider(scheme: string, provider: TextDocumentContentProvider): Disposable; /** - * An event that is emitted when a [text document](#TextDocument) is opened or when the language id - * of a text document [has been changed](#languages.setTextDocumentLanguage). + * An event that is emitted when a {@link TextDocument text document} is opened or when the language id + * of a text document {@link languages.setTextDocumentLanguage has been changed}. * - * To add an event listener when a visible text document is opened, use the [TextEditor](#TextEditor) events in the - * [window](#window) namespace. Note that: + * To add an event listener when a visible text document is opened, use the {@link TextEditor} events in the + * {@link window} namespace. Note that: * - * - The event is emitted before the [document](#TextDocument) is updated in the - * [active text editor](#window.activeTextEditor) - * - When a [text document](#TextDocument) is already open (e.g.: open in another [visible text editor](#window.visibleTextEditors)) this event is not emitted + * - The event is emitted before the {@link TextDocument document} is updated in the + * {@link window.activeTextEditor active text editor} + * - When a {@link TextDocument text document} is already open (e.g.: open in another {@link window.visibleTextEditors visible text editor}) this event is not emitted * */ export const onDidOpenTextDocument: Event; /** - * An event that is emitted when a [text document](#TextDocument) is disposed or when the language id - * of a text document [has been changed](#languages.setTextDocumentLanguage). + * An event that is emitted when a {@link TextDocument text document} is disposed or when the language id + * of a text document {@link languages.setTextDocumentLanguage has been changed}. * * *Note 1:* There is no guarantee that this event fires when an editor tab is closed, use the - * [`onDidChangeVisibleTextEditors`](#window.onDidChangeVisibleTextEditors)-event to know when editors change. + * {@link window.onDidChangeVisibleTextEditors `onDidChangeVisibleTextEditors`}-event to know when editors change. * * *Note 2:* A document can be open but not shown in an editor which means this event can fire * for a document that has not been shown in an editor. @@ -10558,19 +10613,19 @@ declare module 'vscode' { export const onDidCloseTextDocument: Event; /** - * An event that is emitted when a [text document](#TextDocument) is changed. This usually happens - * when the [contents](#TextDocument.getText) changes but also when other things like the - * [dirty](#TextDocument.isDirty)-state changes. + * An event that is emitted when a {@link TextDocument text document} is changed. This usually happens + * when the {@link TextDocument.getText contents} changes but also when other things like the + * {@link TextDocument.isDirty dirty}-state changes. */ export const onDidChangeTextDocument: Event; /** - * An event that is emitted when a [text document](#TextDocument) will be saved to disk. + * An event that is emitted when a {@link TextDocument text document} will be saved to disk. * * *Note 1:* Subscribers can delay saving by registering asynchronous work. For the sake of data integrity the editor * might save without firing this event. For instance when shutting down with dirty files. * - * *Note 2:* Subscribers are called sequentially and they can [delay](#TextDocumentWillSaveEvent.waitUntil) saving + * *Note 2:* Subscribers are called sequentially and they can {@link TextDocumentWillSaveEvent.waitUntil delay} saving * by registering asynchronous work. Protection against misbehaving listeners is implemented as such: * * there is an overall time budget that all listeners share and if that is exhausted no further listener is called * * listeners that take a long time or produce errors frequently will not be called anymore @@ -10580,17 +10635,76 @@ declare module 'vscode' { export const onWillSaveTextDocument: Event; /** - * An event that is emitted when a [text document](#TextDocument) is saved to disk. + * An event that is emitted when a {@link TextDocument text document} is saved to disk. */ export const onDidSaveTextDocument: Event; + /** + * All notebook documents currently known to the editor. + */ + export const notebookDocuments: readonly NotebookDocument[]; + + /** + * Open a notebook. Will return early if this notebook is already {@link notebook.notebookDocuments loaded}. Otherwise + * the notebook is loaded and the {@link notebook.onDidOpenNotebookDocument `onDidOpenNotebookDocument`}-event fires. + * + * *Note* that the lifecycle of the returned notebook is owned by the editor and not by the extension. That means an + * {@link notebook.onDidCloseNotebookDocument `onDidCloseNotebookDocument`}-event can occur at any time after. + * + * *Note* that opening a notebook does not show a notebook editor. This function only returns a notebook document which + * can be showns in a notebook editor but it can also be used for other things. + * + * @param uri The resource to open. + * @returns A promise that resolves to a {@link NotebookDocument notebook} + */ + export function openNotebookDocument(uri: Uri): Thenable; + + /** + * Open an untitled notebook. The editor will prompt the user for a file + * path when the document is to be saved. + * + * @see {@link openNotebookDocument} + * @param notebookType The notebook type that should be used. + * @param content The initial contents of the notebook. + * @returns A promise that resolves to a {@link NotebookDocument notebook}. + */ + export function openNotebookDocument(notebookType: string, content?: NotebookData): Thenable; + + /** + * Register a {@link NotebookSerializer notebook serializer}. + * + * A notebook serializer must be contributed through the `notebooks` extension point. When opening a notebook file, the editor will send + * the `onNotebook:` activation event, and extensions must register their serializer in return. + * + * @param notebookType A notebook. + * @param serializer A notebook serialzier. + * @param options Optional context options that define what parts of a notebook should be persisted + * @return A {@link Disposable} that unregisters this serializer when being disposed. + */ + export function registerNotebookSerializer(notebookType: string, serializer: NotebookSerializer, options?: NotebookDocumentContentOptions): Disposable; + + /** + * An event that is emitted when a {@link NotebookDocument notebook} is opened. + */ + export const onDidOpenNotebookDocument: Event; + + /** + * An event that is emitted when a {@link NotebookDocument notebook} is disposed. + * + * *Note 1:* There is no guarantee that this event fires when an editor tab is closed. + * + * *Note 2:* A notebook can be open but not shown in an editor which means this event can fire + * for a notebook that has not been shown in an editor. + */ + export const onDidCloseNotebookDocument: Event; + /** * An event that is emitted when files are being created. * * *Note 1:* This event is triggered by user gestures, like creating a file from the - * explorer, or from the [`workspace.applyEdit`](#workspace.applyEdit)-api. This event is *not* fired when + * explorer, or from the {@link workspace.applyEdit `workspace.applyEdit`}-api. This event is *not* fired when * files change on disk, e.g triggered by another application, or when using the - * [`workspace.fs`](#FileSystem)-api. + * {@link FileSystem `workspace.fs`}-api. * * *Note 2:* When this event is fired, edits to files that are are being created cannot be applied. */ @@ -10600,9 +10714,9 @@ declare module 'vscode' { * An event that is emitted when files have been created. * * *Note:* This event is triggered by user gestures, like creating a file from the - * explorer, or from the [`workspace.applyEdit`](#workspace.applyEdit)-api, but this event is *not* fired when + * explorer, or from the {@link workspace.applyEdit `workspace.applyEdit`}-api, but this event is *not* fired when * files change on disk, e.g triggered by another application, or when using the - * [`workspace.fs`](#FileSystem)-api. + * {@link FileSystem `workspace.fs`}-api. */ export const onDidCreateFiles: Event; @@ -10610,9 +10724,9 @@ declare module 'vscode' { * An event that is emitted when files are being deleted. * * *Note 1:* This event is triggered by user gestures, like deleting a file from the - * explorer, or from the [`workspace.applyEdit`](#workspace.applyEdit)-api, but this event is *not* fired when + * explorer, or from the {@link workspace.applyEdit `workspace.applyEdit`}-api, but this event is *not* fired when * files change on disk, e.g triggered by another application, or when using the - * [`workspace.fs`](#FileSystem)-api. + * {@link FileSystem `workspace.fs`}-api. * * *Note 2:* When deleting a folder with children only one event is fired. */ @@ -10622,9 +10736,9 @@ declare module 'vscode' { * An event that is emitted when files have been deleted. * * *Note 1:* This event is triggered by user gestures, like deleting a file from the - * explorer, or from the [`workspace.applyEdit`](#workspace.applyEdit)-api, but this event is *not* fired when + * explorer, or from the {@link workspace.applyEdit `workspace.applyEdit`}-api, but this event is *not* fired when * files change on disk, e.g triggered by another application, or when using the - * [`workspace.fs`](#FileSystem)-api. + * {@link FileSystem `workspace.fs`}-api. * * *Note 2:* When deleting a folder with children only one event is fired. */ @@ -10634,9 +10748,9 @@ declare module 'vscode' { * An event that is emitted when files are being renamed. * * *Note 1:* This event is triggered by user gestures, like renaming a file from the - * explorer, and from the [`workspace.applyEdit`](#workspace.applyEdit)-api, but this event is *not* fired when + * explorer, and from the {@link workspace.applyEdit `workspace.applyEdit`}-api, but this event is *not* fired when * files change on disk, e.g triggered by another application, or when using the - * [`workspace.fs`](#FileSystem)-api. + * {@link FileSystem `workspace.fs`}-api. * * *Note 2:* When renaming a folder with children only one event is fired. */ @@ -10646,9 +10760,9 @@ declare module 'vscode' { * An event that is emitted when files have been renamed. * * *Note 1:* This event is triggered by user gestures, like renaming a file from the - * explorer, and from the [`workspace.applyEdit`](#workspace.applyEdit)-api, but this event is *not* fired when + * explorer, and from the {@link workspace.applyEdit `workspace.applyEdit`}-api, but this event is *not* fired when * files change on disk, e.g triggered by another application, or when using the - * [`workspace.fs`](#FileSystem)-api. + * {@link FileSystem `workspace.fs`}-api. * * *Note 2:* When renaming a folder with children only one event is fired. */ @@ -10670,7 +10784,7 @@ declare module 'vscode' { export function getConfiguration(section?: string | undefined, scope?: ConfigurationScope | null): WorkspaceConfiguration; /** - * An event that is emitted when the [configuration](#WorkspaceConfiguration) changed. + * An event that is emitted when the {@link WorkspaceConfiguration configuration} changed. */ export const onDidChangeConfiguration: Event; @@ -10681,7 +10795,7 @@ declare module 'vscode' { * * @param type The task kind type this provider is registered for. * @param provider A task provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerTaskProvider(type: string, provider: TaskProvider): Disposable; @@ -10691,10 +10805,10 @@ declare module 'vscode' { * There can only be one provider per scheme and an error is being thrown when a scheme * has been claimed by another provider or when it is reserved. * - * @param scheme The uri-[scheme](#Uri.scheme) the provider registers for. + * @param scheme The uri-{@link Uri.scheme scheme} the provider registers for. * @param provider The filesystem provider. * @param options Immutable metadata about the provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerFileSystemProvider(scheme: string, provider: FileSystemProvider, options?: { readonly isCaseSensitive?: boolean, readonly isReadonly?: boolean }): Disposable; @@ -10712,8 +10826,8 @@ declare module 'vscode' { /** * The configuration scope which can be a * a 'resource' or a languageId or both or - * a '[TextDocument](#TextDocument)' or - * a '[WorkspaceFolder](#WorkspaceFolder)' + * a '{@link TextDocument}' or + * a '{@link WorkspaceFolder}' */ export type ConfigurationScope = Uri | TextDocument | WorkspaceFolder | { uri?: Uri, languageId: string }; @@ -10743,7 +10857,7 @@ declare module 'vscode' { * * The editor provides an API that makes it simple to provide such common features by having all UI and actions already in place and * by allowing you to participate by providing data only. For instance, to contribute a hover all you have to do is provide a function - * that can be called with a [TextDocument](#TextDocument) and a [Position](#Position) returning hover info. The rest, like tracking the + * that can be called with a {@link TextDocument} and a {@link Position} returning hover info. The rest, like tracking the * mouse, positioning the hover, keeping the hover stable etc. is taken care of by the editor. * * ```javascript @@ -10754,11 +10868,11 @@ declare module 'vscode' { * }); * ``` * - * Registration is done using a [document selector](#DocumentSelector) which is either a language id, like `javascript` or - * a more complex [filter](#DocumentFilter) like `{ language: 'typescript', scheme: 'file' }`. Matching a document against such - * a selector will result in a [score](#languages.match) that is used to determine if and how a provider shall be used. When - * scores are equal the provider that came last wins. For features that allow full arity, like [hover](#languages.registerHoverProvider), - * the score is only checked to be `>0`, for other features, like [IntelliSense](#languages.registerCompletionItemProvider) the + * Registration is done using a {@link DocumentSelector document selector} which is either a language id, like `javascript` or + * a more complex {@link DocumentFilter filter} like `{ language: 'typescript', scheme: 'file' }`. Matching a document against such + * a selector will result in a {@link languages.match score} that is used to determine if and how a provider shall be used. When + * scores are equal the provider that came last wins. For features that allow full arity, like {@link languages.registerHoverProvider hover}, + * the score is only checked to be `>0`, for other features, like {@link languages.registerCompletionItemProvider IntelliSense} the * score is used for determining the order in which providers are asked to participate. */ export namespace languages { @@ -10770,11 +10884,11 @@ declare module 'vscode' { export function getLanguages(): Thenable; /** - * Set (and change) the [language](#TextDocument.languageId) that is associated + * Set (and change) the {@link TextDocument.languageId language} that is associated * with the given document. * - * *Note* that calling this function will trigger the [`onDidCloseTextDocument`](#workspace.onDidCloseTextDocument) event - * followed by the [`onDidOpenTextDocument`](#workspace.onDidOpenTextDocument) event. + * *Note* that calling this function will trigger the {@link workspace.onDidCloseTextDocument `onDidCloseTextDocument`} event + * followed by the {@link workspace.onDidOpenTextDocument `onDidOpenTextDocument`} event. * * @param document The document which language is to be changed * @param languageId The new language identifier. @@ -10783,13 +10897,13 @@ declare module 'vscode' { export function setTextDocumentLanguage(document: TextDocument, languageId: string): Thenable; /** - * Compute the match between a document [selector](#DocumentSelector) and a document. Values + * Compute the match between a document {@link DocumentSelector selector} and a document. Values * greater than zero mean the selector matches the document. * * A match is computed according to these rules: - * 1. When [`DocumentSelector`](#DocumentSelector) is an array, compute the match for each contained `DocumentFilter` or language identifier and take the maximum value. - * 2. A string will be desugared to become the `language`-part of a [`DocumentFilter`](#DocumentFilter), so `"fooLang"` is like `{ language: "fooLang" }`. - * 3. A [`DocumentFilter`](#DocumentFilter) will be matched against the document by comparing its parts with the document. The following rules apply: + * 1. When {@link DocumentSelector `DocumentSelector`} is an array, compute the match for each contained `DocumentFilter` or language identifier and take the maximum value. + * 2. A string will be desugared to become the `language`-part of a {@link DocumentFilter `DocumentFilter`}, so `"fooLang"` is like `{ language: "fooLang" }`. + * 3. A {@link DocumentFilter `DocumentFilter`} will be matched against the document by comparing its parts with the document. The following rules apply: * 1. When the `DocumentFilter` is empty (`{}`) the result is `0` * 2. When `scheme`, `language`, or `pattern` are defined but one doesn’t match, the result is `0` * 3. Matching against `*` gives a score of `5`, matching via equality or via a glob-pattern gives a score of `10` @@ -10822,7 +10936,7 @@ declare module 'vscode' { export function match(selector: DocumentSelector, document: TextDocument): number; /** - * An [event](#Event) which fires when the global set of diagnostics changes. This is + * An {@link Event} which fires when the global set of diagnostics changes. This is * newly added and removed diagnostics. */ export const onDidChangeDiagnostics: Event; @@ -10831,7 +10945,7 @@ declare module 'vscode' { * Get all diagnostics for a given resource. * * @param resource A resource - * @returns An array of [diagnostics](#Diagnostic) objects or an empty array. + * @returns An array of {@link Diagnostic diagnostics} objects or an empty array. */ export function getDiagnostics(resource: Uri): Diagnostic[]; @@ -10845,7 +10959,7 @@ declare module 'vscode' { /** * Create a diagnostics collection. * - * @param name The [name](#DiagnosticCollection.name) of the collection. + * @param name The {@link DiagnosticCollection.name name} of the collection. * @return A new diagnostic collection. */ export function createDiagnosticCollection(name?: string): DiagnosticCollection; @@ -10854,20 +10968,20 @@ declare module 'vscode' { * Register a completion provider. * * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and groups of equal score are sequentially asked for + * by their {@link languages.match score} and groups of equal score are sequentially asked for * completion items. The process stops when one or many providers of a group return a * result. A failing provider (rejected promise or exception) will not fail the whole * operation. * * A completion item provider can be associated with a set of `triggerCharacters`. When trigger * characters are being typed, completions are requested but only from providers that registered - * the typed character. Because of that trigger characters should be different than [word characters](#LanguageConfiguration.wordPattern), + * the typed character. Because of that trigger characters should be different than {@link LanguageConfiguration.wordPattern word characters}, * a common trigger character is `.` to trigger member completions. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A completion provider. * @param triggerCharacters Trigger completion when the user types one of the characters. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerCompletionItemProvider(selector: DocumentSelector, provider: CompletionItemProvider, ...triggerCharacters: string[]): Disposable; @@ -10881,7 +10995,7 @@ declare module 'vscode' { * @param selector A selector that defines the documents this provider is applicable to. * @param provider A code action provider. * @param metadata Metadata about the kind of code actions the provider provides. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerCodeActionsProvider(selector: DocumentSelector, provider: CodeActionProvider, metadata?: CodeActionProviderMetadata): Disposable; @@ -10894,7 +11008,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A code lens provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerCodeLensProvider(selector: DocumentSelector, provider: CodeLensProvider): Disposable; @@ -10907,7 +11021,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A definition provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerDefinitionProvider(selector: DocumentSelector, provider: DefinitionProvider): Disposable; @@ -10920,7 +11034,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider An implementation provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerImplementationProvider(selector: DocumentSelector, provider: ImplementationProvider): Disposable; @@ -10933,7 +11047,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A type definition provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerTypeDefinitionProvider(selector: DocumentSelector, provider: TypeDefinitionProvider): Disposable; @@ -10946,7 +11060,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A declaration provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerDeclarationProvider(selector: DocumentSelector, provider: DeclarationProvider): Disposable; @@ -10959,25 +11073,25 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A hover provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerHoverProvider(selector: DocumentSelector, provider: HoverProvider): Disposable; /** * Register a provider that locates evaluatable expressions in text documents. - * VS Code will evaluate the expression in the active debug session and will show the result in the debug hover. + * The editor will evaluate the expression in the active debug session and will show the result in the debug hover. * * If multiple providers are registered for a language an arbitrary provider will be used. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider An evaluatable expression provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerEvaluatableExpressionProvider(selector: DocumentSelector, provider: EvaluatableExpressionProvider): Disposable; /** * Register a provider that returns data for the debugger's 'inline value' feature. - * Whenever the generic VS Code debugger has stopped in a source file, providers registered for the language of the file + * Whenever the generic debugger has stopped in a source file, providers registered for the language of the file * are called to return textual data that will be shown in the editor at the end of lines. * * Multiple providers can be registered for a language. In that case providers are asked in @@ -10986,7 +11100,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider An inline values provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerInlineValuesProvider(selector: DocumentSelector, provider: InlineValuesProvider): Disposable; @@ -10994,12 +11108,12 @@ declare module 'vscode' { * Register a document highlight provider. * * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and groups sequentially asked for document highlights. + * by their {@link languages.match score} and groups sequentially asked for document highlights. * The process stops when a provider returns a `non-falsy` or `non-failure` result. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A document highlight provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerDocumentHighlightProvider(selector: DocumentSelector, provider: DocumentHighlightProvider): Disposable; @@ -11013,7 +11127,7 @@ declare module 'vscode' { * @param selector A selector that defines the documents this provider is applicable to. * @param provider A document symbol provider. * @param metaData metadata about the provider - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerDocumentSymbolProvider(selector: DocumentSelector, provider: DocumentSymbolProvider, metaData?: DocumentSymbolProviderMetadata): Disposable; @@ -11025,7 +11139,7 @@ declare module 'vscode' { * a failure of the whole operation. * * @param provider A workspace symbol provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerWorkspaceSymbolProvider(provider: WorkspaceSymbolProvider): Disposable; @@ -11038,7 +11152,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A reference provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerReferenceProvider(selector: DocumentSelector, provider: ReferenceProvider): Disposable; @@ -11046,12 +11160,12 @@ declare module 'vscode' { * Register a rename provider. * * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and asked in sequence. The first provider producing a result + * by their {@link languages.match score} and asked in sequence. The first provider producing a result * defines the result of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A rename provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerRenameProvider(selector: DocumentSelector, provider: RenameProvider): Disposable; @@ -11059,12 +11173,12 @@ declare module 'vscode' { * Register a semantic tokens provider for a whole document. * * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and the best-matching provider is used. Failure + * by their {@link languages.match score} and the best-matching provider is used. Failure * of the selected provider will cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A document semantic tokens provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerDocumentSemanticTokensProvider(selector: DocumentSelector, provider: DocumentSemanticTokensProvider, legend: SemanticTokensLegend): Disposable; @@ -11078,12 +11192,12 @@ declare module 'vscode' { * will be used. * * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and the best-matching provider is used. Failure + * by their {@link languages.match score} and the best-matching provider is used. Failure * of the selected provider will cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A document range semantic tokens provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerDocumentRangeSemanticTokensProvider(selector: DocumentSelector, provider: DocumentRangeSemanticTokensProvider, legend: SemanticTokensLegend): Disposable; @@ -11091,29 +11205,29 @@ declare module 'vscode' { * Register a formatting provider for a document. * * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and the best-matching provider is used. Failure + * by their {@link languages.match score} and the best-matching provider is used. Failure * of the selected provider will cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A document formatting edit provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerDocumentFormattingEditProvider(selector: DocumentSelector, provider: DocumentFormattingEditProvider): Disposable; /** * Register a formatting provider for a document range. * - * *Note:* A document range provider is also a [document formatter](#DocumentFormattingEditProvider) - * which means there is no need to [register](#languages.registerDocumentFormattingEditProvider) a document + * *Note:* A document range provider is also a {@link DocumentFormattingEditProvider document formatter} + * which means there is no need to {@link languages.registerDocumentFormattingEditProvider register} a document * formatter when also registering a range provider. * * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and the best-matching provider is used. Failure + * by their {@link languages.match score} and the best-matching provider is used. Failure * of the selected provider will cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A document range formatting edit provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerDocumentRangeFormattingEditProvider(selector: DocumentSelector, provider: DocumentRangeFormattingEditProvider): Disposable; @@ -11121,14 +11235,14 @@ declare module 'vscode' { * Register a formatting provider that works on type. The provider is active when the user enables the setting `editor.formatOnType`. * * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and the best-matching provider is used. Failure + * by their {@link languages.match score} and the best-matching provider is used. Failure * of the selected provider will cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider An on type formatting edit provider. * @param firstTriggerCharacter A character on which formatting should be triggered, like `}`. * @param moreTriggerCharacter More trigger characters. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerOnTypeFormattingEditProvider(selector: DocumentSelector, provider: OnTypeFormattingEditProvider, firstTriggerCharacter: string, ...moreTriggerCharacter: string[]): Disposable; @@ -11136,14 +11250,14 @@ declare module 'vscode' { * Register a signature help provider. * * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and called sequentially until a provider returns a + * by their {@link languages.match score} and called sequentially until a provider returns a * valid result. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A signature help provider. * @param triggerCharacters Trigger signature help when the user types one of the characters, like `,` or `(`. * @param metadata Information about the provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerSignatureHelpProvider(selector: DocumentSelector, provider: SignatureHelpProvider, ...triggerCharacters: string[]): Disposable; export function registerSignatureHelpProvider(selector: DocumentSelector, provider: SignatureHelpProvider, metadata: SignatureHelpProviderMetadata): Disposable; @@ -11157,7 +11271,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A document link provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerDocumentLinkProvider(selector: DocumentSelector, provider: DocumentLinkProvider): Disposable; @@ -11170,7 +11284,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A color provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerColorProvider(selector: DocumentSelector, provider: DocumentColorProvider): Disposable; @@ -11187,7 +11301,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A folding range provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerFoldingRangeProvider(selector: DocumentSelector, provider: FoldingRangeProvider): Disposable; @@ -11200,7 +11314,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A selection range provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerSelectionRangeProvider(selector: DocumentSelector, provider: SelectionRangeProvider): Disposable; @@ -11209,7 +11323,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A call hierarchy provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerCallHierarchyProvider(selector: DocumentSelector, provider: CallHierarchyProvider): Disposable; @@ -11217,26 +11331,825 @@ declare module 'vscode' { * Register a linked editing range provider. * * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and the best-matching provider that has a result is used. Failure + * by their {@link languages.match score} and the best-matching provider that has a result is used. Failure * of the selected provider will cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A linked editing range provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerLinkedEditingRangeProvider(selector: DocumentSelector, provider: LinkedEditingRangeProvider): Disposable; /** - * Set a [language configuration](#LanguageConfiguration) for a language. + * Set a {@link LanguageConfiguration language configuration} for a language. * * @param language A language identifier like `typescript`. * @param configuration Language configuration. - * @return A [disposable](#Disposable) that unsets this configuration. + * @return A {@link Disposable} that unsets this configuration. */ export function setLanguageConfiguration(language: string, configuration: LanguageConfiguration): Disposable; } + /** + * A notebook cell kind. + */ + export enum NotebookCellKind { + + /** + * A markup-cell is formatted source that is used for display. + */ + Markup = 1, + + /** + * A code-cell is source that can be {@link NotebookController executed} and that + * produces {@link NotebookCellOutput output}. + */ + Code = 2 + } + + /** + * Represents a cell of a {@link NotebookDocument notebook}, either a {@link NotebookCellKind.Code code}-cell + * or {@link NotebookCellKind.Markup markup}-cell. + * + * NotebookCell instances are immutable and are kept in sync for as long as they are part of their notebook. + */ + export interface NotebookCell { + + /** + * The index of this cell in its {@link NotebookDocument.cellAt containing notebook}. The + * index is updated when a cell is moved within its notebook. The index is `-1` + * when the cell has been removed from its notebook. + */ + readonly index: number; + + /** + * The {@link NotebookDocument notebook} that contains this cell. + */ + readonly notebook: NotebookDocument; + + /** + * The kind of this cell. + */ + readonly kind: NotebookCellKind; + + /** + * The {@link TextDocument text} of this cell, represented as text document. + */ + readonly document: TextDocument; + + /** + * The metadata of this cell. Can be anything but must be JSON-stringifyable. + */ + readonly metadata: { [key: string]: any } + + /** + * The outputs of this cell. + */ + readonly outputs: readonly NotebookCellOutput[]; + + /** + * The most recent {@link NotebookCellExecutionSummary excution summary} for this cell. + */ + readonly executionSummary?: NotebookCellExecutionSummary; + } + + /** + * Represents a notebook which itself is a sequence of {@link NotebookCell code or markup cells}. Notebook documents are + * created from {@link NotebookData notebook data}. + */ + export interface NotebookDocument { + + /** + * The associated uri for this notebook. + * + * *Note* that most notebooks use the `file`-scheme, which means they are files on disk. However, **not** all notebooks are + * saved on disk and therefore the `scheme` must be checked before trying to access the underlying file or siblings on disk. + * + * @see {@link FileSystemProvider} + */ + readonly uri: Uri; + + /** + * The type of notebook. + */ + readonly notebookType: string; + + /** + * The version number of this notebook (it will strictly increase after each + * change, including undo/redo). + */ + readonly version: number; + + /** + * `true` if there are unpersisted changes. + */ + readonly isDirty: boolean; + + /** + * Is this notebook representing an untitled file which has not been saved yet. + */ + readonly isUntitled: boolean; + + /** + * `true` if the notebook has been closed. A closed notebook isn't synchronized anymore + * and won't be re-used when the same resource is opened again. + */ + readonly isClosed: boolean; + + /** + * Arbitrary metadata for this notebook. Can be anything but must be JSON-stringifyable. + */ + readonly metadata: { [key: string]: any }; + + /** + * The number of cells in the notebook. + */ + readonly cellCount: number; + + /** + * Return the cell at the specified index. The index will be adjusted to the notebook. + * + * @param index - The index of the cell to retrieve. + * @return A {@link NotebookCell cell}. + */ + cellAt(index: number): NotebookCell; + + /** + * Get the cells of this notebook. A subset can be retrieved by providing + * a range. The range will be adjuset to the notebook. + * + * @param range A notebook range. + * @returns The cells contained by the range or all cells. + */ + getCells(range?: NotebookRange): NotebookCell[]; + + /** + * Save the document. The saving will be handled by the corresponding {@link NotebookSerializer serializer}. + * + * @return A promise that will resolve to true when the document + * has been saved. Will return false if the file was not dirty or when save failed. + */ + save(): Thenable; + } + + /** + * The summary of a notebook cell execution. + */ + export interface NotebookCellExecutionSummary { + + /** + * The order in which the execution happened. + */ + readonly executionOrder?: number; + + /** + * If the exclusive finished successfully. + */ + readonly success?: boolean; + + /** + * The times at which execution started and ended, as unix timestamps + */ + readonly timing?: { startTime: number, endTime: number }; + } + + /** + * A notebook range represents an ordered pair of two cell indicies. + * It is guaranteed that start is less than or equal to end. + */ + export class NotebookRange { + + /** + * The zero-based start index of this range. + */ + readonly start: number; + + /** + * The exclusive end index of this range (zero-based). + */ + readonly end: number; + + /** + * `true` if `start` and `end` are equal. + */ + readonly isEmpty: boolean; + + /** + * Create a new notebook range. If `start` is not + * before or equal to `end`, the values will be swapped. + * + * @param start start index + * @param end end index. + */ + constructor(start: number, end: number); + + /** + * Derive a new range for this range. + * + * @param change An object that describes a change to this range. + * @return A range that reflects the given change. Will return `this` range if the change + * is not changing anything. + */ + with(change: { start?: number, end?: number }): NotebookRange; + } + + /** + * One representation of a {@link NotebookCellOutput notebook output}, defined by MIME type and data. + */ + export class NotebookCellOutputItem { + + /** + * Factory function to create a `NotebookCellOutputItem` from a string. + * + * *Note* that an UTF-8 encoder is used to create bytes for the string. + * + * @param value A string. + * @param mime Optional MIME type, defaults to `text/plain`. + * @returns A new output item object. + */ + static text(value: string, mime?: string): NotebookCellOutputItem; + + /** + * Factory function to create a `NotebookCellOutputItem` from + * a JSON object. + * + * *Note* that this function is not expecting "stringified JSON" but + * an object that can be stringified. This function will throw an error + * when the passed value cannot be JSON-stringified. + * + * @param value A JSON-stringifyable value. + * @param mime Optional MIME type, defaults to `application/json` + * @returns A new output item object. + */ + static json(value: any, mime?: string): NotebookCellOutputItem; + + /** + * Factory function to create a `NotebookCellOutputItem` that uses + * uses the `application/vnd.code.notebook.stdout` mime type. + * + * @param value A string. + * @returns A new output item object. + */ + static stdout(value: string): NotebookCellOutputItem; + + /** + * Factory function to create a `NotebookCellOutputItem` that uses + * uses the `application/vnd.code.notebook.stderr` mime type. + * + * @param value A string. + * @returns A new output item object. + */ + static stderr(value: string): NotebookCellOutputItem; + + /** + * Factory function to create a `NotebookCellOutputItem` that uses + * uses the `application/vnd.code.notebook.error` mime type. + * + * @param value An error object. + * @returns A new output item object. + */ + static error(value: Error): NotebookCellOutputItem; + + /** + * The mime type which determines how the {@link NotebookCellOutputItem.data `data`}-property + * is interpreted. + * + * Notebooks have built-in support for certain mime-types, extensions can add support for new + * types and override existing types. + */ + mime: string; + + /** + * The data of this output item. Must always be an array of unsigned 8-bit integers. + */ + data: Uint8Array; + + /** + * Create a new notbook cell output item. + * + * @param data The value of the output item. + * @param mime The mime type of the output item. + */ + constructor(data: Uint8Array, mime: string); + } + + /** + * Notebook cell output represents a result of executing a cell. It is a container type for multiple + * {@link NotebookCellOutputItem output items} where contained items represent the same result but + * use different MIME types. + */ + export class NotebookCellOutput { + + /** + * The output items of this output. Each item must represent the same result. _Note_ that repeated + * MIME types per output is invalid and that the editor will just pick one of them. + * + * ```ts + * new vscode.NotebookCellOutput([ + * vscode.NotebookCellOutputItem.text('Hello', 'text/plain'), + * vscode.NotebookCellOutputItem.text('Hello', 'text/html'), + * vscode.NotebookCellOutputItem.text('_Hello_', 'text/markdown'), + * vscode.NotebookCellOutputItem.text('Hey', 'text/plain'), // INVALID: repeated type, editor will pick just one + * ]) + * ``` + */ + items: NotebookCellOutputItem[]; + + /** + * Arbitrary metadata for this cell output. Can be anything but must be JSON-stringifyable. + */ + metadata?: { [key: string]: any }; + + /** + * Create new notebook output. + * + * @param items Notebook output items. + * @param metadata Optional metadata. + */ + constructor(items: NotebookCellOutputItem[], metadata?: { [key: string]: any }); + } + + /** + * NotebookCellData is the raw representation of notebook cells. Its is part of {@link NotebookData `NotebookData`}. + */ + export class NotebookCellData { + + /** + * The {@link NotebookCellKind kind} of this cell data. + */ + kind: NotebookCellKind; + + /** + * The source value of this cell data - either source code or formatted text. + */ + value: string; + + /** + * The language identifier of the source value of this cell data. Any value from + * {@link languages.getLanguages `getLanguages`} is possible. + */ + languageId: string; + + /** + * The outputs of this cell data. + */ + outputs?: NotebookCellOutput[]; + + /** + * Arbitrary metadata of this cell data. Can be anything but must be JSON-stringifyable. + */ + metadata?: { [key: string]: any }; + + /** + * The execution summary of this cell data. + */ + executionSummary?: NotebookCellExecutionSummary; + + /** + * Create new cell data. Minimal cell data specifies its kind, its source value, and the + * language identifier of its source. + * + * @param kind The kind. + * @param value The source value. + * @param languageId The language identifier of the source value. + */ + constructor(kind: NotebookCellKind, value: string, languageId: string); + } + + /** + * NotebookData is the raw representation of notebooks. + * + * Extensions are responsible to create {@link NotebookData `NotebookData`} so that the editor + * can create a {@link NotebookDocument `NotebookDocument`}. + * + * @see {@link NotebookSerializer} + */ + export class NotebookData { + /** + * The cell data of this notebook data. + */ + cells: NotebookCellData[]; + + /** + * Arbitrary metadata of notebook data. + */ + metadata?: { [key: string]: any }; + + /** + * Create new notebook data. + * + * @param cells An array of cell data. + */ + constructor(cells: NotebookCellData[]); + } + + /** + * The notebook serializer enables the editor to open notebook files. + * + * At its core the editor only knows a {@link NotebookData notebook data structure} but not + * how that data structure is written to a file, nor how it is read from a file. The + * notebook serializer bridges this gap by deserializing bytes into notebook data and + * vice versa. + */ + export interface NotebookSerializer { + + /** + * Deserialize contents of a notebook file into the notebook data structure. + * + * @param content Contents of a notebook file. + * @param token A cancellation token. + * @return Notebook data or a thenable that resolves to such. + */ + deserializeNotebook(content: Uint8Array, token: CancellationToken): NotebookData | Thenable; + + /** + * Serialize notebook data into file contents. + * + * @param data A notebook data structure. + * @param token A cancellation token. + * @returns An array of bytes or a thenable that resolves to such. + */ + serializeNotebook(data: NotebookData, token: CancellationToken): Uint8Array | Thenable; + } + + /** + * Notebook content options define what parts of a notebook are persisted. Note + * + * For instance, a notebook serializer can opt-out of saving outputs and in that case the editor doesn't mark a + * notebooks as {@link NotebookDocument.isDirty dirty} when its output has changed. + */ + export interface NotebookDocumentContentOptions { + /** + * Controls if outputs change will trigger notebook document content change and if it will be used in the diff editor + * Default to false. If the content provider doesn't persisit the outputs in the file document, this should be set to true. + */ + transientOutputs?: boolean; + + /** + * Controls if a cell metadata property change will trigger notebook document content change and if it will be used in the diff editor + * Default to false. If the content provider doesn't persisit a metadata property in the file document, it should be set to true. + */ + transientCellMetadata?: { [key: string]: boolean | undefined }; + + /** + * Controls if a document metadata property change will trigger notebook document content change and if it will be used in the diff editor + * Default to false. If the content provider doesn't persisit a metadata property in the file document, it should be set to true. + */ + transientDocumentMetadata?: { [key: string]: boolean | undefined }; + } + + /** + * Notebook controller affinity for notebook documents. + * + * @see {@link NotebookController.updateNotebookAffinity} + */ + export enum NotebookControllerAffinity { + /** + * Default affinity. + */ + Default = 1, + /** + * A controller is preferred for a notebook. + */ + Preferred = 2 + } + + /** + * A notebook controller represents an entity that can execute notebook cells. This is often referred to as a kernel. + * + * There can be multiple controllers and the editor will let users choose which controller to use for a certain notebook. The + * {@link NotebookController.notebookType `notebookType`}-property defines for what kind of notebooks a controller is for and + * the {@link NotebookController.updateNotebookAffinity `updateNotebookAffinity`}-function allows controllers to set a preference + * for specific notebook documents. When a controller has been selected its + * {@link NotebookController.onDidChangeSelectedNotebooks onDidChangeSelectedNotebooks}-event fires. + * + * When a cell is being run the editor will invoke the {@link NotebookController.executeHandler `executeHandler`} and a controller + * is expected to create and finalize a {@link NotebookCellExecution notebook cell execution}. However, controllers are also free + * to create executions by themselves. + */ + export interface NotebookController { + + /** + * The identifier of this notebook controller. + * + * _Note_ that controllers are remembered by their identifier and that extensions should use + * stable identifiers across sessions. + */ + readonly id: string; + + /** + * The notebook type this controller is for. + */ + readonly notebookType: string; + + /** + * An array of language identifiers that are supported by this + * controller. Any language identifier from {@link languages.getLanguages `getLanguages`} + * is possible. When falsy all languages are supported. + * + * Samples: + * ```js + * // support JavaScript and TypeScript + * myController.supportedLanguages = ['javascript', 'typescript'] + * + * // support all languages + * myController.supportedLanguages = undefined; // falsy + * myController.supportedLanguages = []; // falsy + * ``` + */ + supportedLanguages?: string[]; + + /** + * The human-readable label of this notebook controller. + */ + label: string; + + /** + * The human-readable description which is rendered less prominent. + */ + description?: string; + + /** + * The human-readable detail which is rendered less prominent. + */ + detail?: string; + + /** + * Whether this controller supports execution order so that the + * editor can render placeholders for them. + */ + supportsExecutionOrder?: boolean; + + /** + * Create a cell execution task. + * + * _Note_ that there can only be one execution per cell at a time and that an error is thrown if + * a cell execution is created while another is still active. + * + * This should be used in response to the {@link NotebookController.executeHandler execution handler} + * being called or when cell execution has been started else, e.g when a cell was already + * executing or when cell execution was triggered from another source. + * + * @param cell The notebook cell for which to create the execution. + * @returns A notebook cell execution. + */ + createNotebookCellExecution(cell: NotebookCell): NotebookCellExecution; + + /** + * The execute handler is invoked when the run gestures in the UI are selected, e.g Run Cell, Run All, + * Run Selection etc. The execute handler is responsible for creating and managing {@link NotebookCellExecution execution}-objects. + */ + executeHandler: (cells: NotebookCell[], notebook: NotebookDocument, controller: NotebookController) => void | Thenable; + + /** + * Optional interrupt handler. + * + * By default cell execution is canceled via {@link NotebookCellExecution.token tokens}. Cancellation + * tokens require that a controller can keep track of its execution so that it can cancel a specific execution at a later + * point. Not all scenarios allow for that, eg. REPL-style controllers often work by interrupting whatever is currently + * running. For those cases the interrupt handler exists - it can be thought of as the equivalent of `SIGINT` + * or `Control+C` in terminals. + * + * _Note_ that supporting {@link NotebookCellExecution.token cancellation tokens} is preferred and that interrupt handlers should + * only be used when tokens cannot be supported. + */ + interruptHandler?: (notebook: NotebookDocument) => void | Thenable; + + /** + * An event that fires whenever a controller has been selected or un-selected for a notebook document. + * + * There can be multiple controllers for a notebook and in that case a controllers needs to be _selected_. This is a user gesture + * and happens either explicitly or implicitly when interacting with a notebook for which a controller was _suggested_. When possible, + * the editor _suggests_ a controller that is most likely to be _selected_. + * + * _Note_ that controller selection is persisted (by the controllers {@link NotebookController.id id}) and restored as soon as a + * controller is re-created or as a notebook is {@link workspace.onDidOpenNotebookDocument opened}. + */ + readonly onDidChangeSelectedNotebooks: Event<{ notebook: NotebookDocument, selected: boolean }>; + + /** + * A controller can set affinities for specific notebook documents. This allows a controller + * to be presented more prominent for some notebooks. + * + * @param notebook The notebook for which a priority is set. + * @param affinity A controller affinity + */ + updateNotebookAffinity(notebook: NotebookDocument, affinity: NotebookControllerAffinity): void; + + /** + * Dispose and free associated resources. + */ + dispose(): void; + } + + /** + * A NotebookCellExecution is how {@link NotebookController notebook controller} modify a notebook cell as + * it is executing. + * + * When a cell execution object is created, the cell enters the {@link NotebookCellExecutionState.Pending `Pending`} state. + * When {@link NotebookCellExecution.start `start(...)`} is called on the execution task, it enters the {@link NotebookCellExecutionState.Executing `Executing`} state. When + * {@link NotebookCellExecution.end `end(...)`} is called, it enters the {@link NotebookCellExecutionState.Idle `Idle`} state. + */ + export interface NotebookCellExecution { + + /** + * The {@link NotebookCell cell} for which this execution has been created. + */ + readonly cell: NotebookCell; + + /** + * A cancellation token which will be triggered when the cell execution is canceled + * from the UI. + * + * _Note_ that the cancellation token will not be triggered when the {@link NotebookController controller} + * that created this execution uses an {@link NotebookController.interruptHandler interrupt-handler}. + */ + readonly token: CancellationToken; + + /** + * Set and unset the order of this cell execution. + */ + executionOrder: number | undefined; + + /** + * Signal that the execution has begun. + * + * @param startTime The time that execution began, in milliseconds in the Unix epoch. Used to drive the clock + * that shows for how long a cell has been running. If not given, the clock won't be shown. + */ + start(startTime?: number): void; + + /** + * Signal that execution has ended. + * + * @param success If true, a green check is shown on the cell status bar. + * If false, a red X is shown. + * If undefined, no check or X icon is shown. + * @param endTime The time that execution finished, in milliseconds in the Unix epoch. + */ + end(success: boolean | undefined, endTime?: number): void; + + /** + * Clears the output of the cell that is executing or of another cell that is affected by this execution. + * + * @param cell Cell for which output is cleared. Defaults to the {@link NotebookCellExecution.cell cell} of + * this execution. + * @return A thenable that resolves when the operation finished. + */ + clearOutput(cell?: NotebookCell): Thenable; + + /** + * Replace the output of the cell that is executing or of another cell that is affected by this execution. + * + * @param out Output that replaces the current output. + * @param cell Cell for which output is cleared. Defaults to the {@link NotebookCellExecution.cell cell} of + * this execution. + * @return A thenable that resolves when the operation finished. + */ + replaceOutput(out: NotebookCellOutput | NotebookCellOutput[], cell?: NotebookCell): Thenable; + + /** + * Append to the output of the cell that is executing or to another cell that is affected by this execution. + * + * @param out Output that is appended to the current output. + * @param cell Cell for which output is cleared. Defaults to the {@link NotebookCellExecution.cell cell} of + * this execution. + * @return A thenable that resolves when the operation finished. + */ + appendOutput(out: NotebookCellOutput | NotebookCellOutput[], cell?: NotebookCell): Thenable; + + /** + * Replace all output items of existing cell output. + * + * @param items Output items that replace the items of existing output. + * @param output Output object that already exists. + * @return A thenable that resolves when the operation finished. + */ + replaceOutputItems(items: NotebookCellOutputItem | NotebookCellOutputItem[], output: NotebookCellOutput): Thenable; + + /** + * Append output items to existing cell output. + * + * @param items Output items that are append to existing output. + * @param output Output object that already exists. + * @return A thenable that resolves when the operation finished. + */ + appendOutputItems(items: NotebookCellOutputItem | NotebookCellOutputItem[], output: NotebookCellOutput): Thenable; + } + + /** + * Represents the alignment of status bar items. + */ + export enum NotebookCellStatusBarAlignment { + + /** + * Aligned to the left side. + */ + Left = 1, + + /** + * Aligned to the right side. + */ + Right = 2 + } + + /** + * A contribution to a cell's status bar + */ + export class NotebookCellStatusBarItem { + /** + * The text to show for the item. + */ + text: string; + + /** + * Whether the item is aligned to the left or right. + */ + alignment: NotebookCellStatusBarAlignment; + + /** + * An optional {@link Command `Command`} or identifier of a command to run on click. + * + * The command must be {@link commands.getCommands known}. + * + * Note that if this is a {@link Command `Command`} object, only the {@link Command.command `command`} and {@link Command.arguments `arguments`} + * are used by the editor. + */ + command?: string | Command; + + /** + * A tooltip to show when the item is hovered. + */ + tooltip?: string; + + /** + * The priority of the item. A higher value item will be shown more to the left. + */ + priority?: number; + + /** + * Accessibility information used when a screen reader interacts with this item. + */ + accessibilityInformation?: AccessibilityInformation; + + /** + * Creates a new NotebookCellStatusBarItem. + * @param text The text to show for the item. + * @param alignment Whether the item is aligned to the left or right. + */ + constructor(text: string, alignment: NotebookCellStatusBarAlignment); + } + + /** + * A provider that can contribute items to the status bar that appears below a cell's editor. + */ + export interface NotebookCellStatusBarItemProvider { + /** + * An optional event to signal that statusbar items have changed. The provide method will be called again. + */ + onDidChangeCellStatusBarItems?: Event; + + /** + * The provider will be called when the cell scrolls into view, when its content, outputs, language, or metadata change, and when it changes execution state. + * @param cell The cell for which to return items. + * @param token A token triggered if this request should be cancelled. + * @return One or more {@link NotebookCellStatusBarItem cell statusbar items} + */ + provideCellStatusBarItems(cell: NotebookCell, token: CancellationToken): ProviderResult; + } + + /** + * Namespace for notebooks. + * + * The notebooks functionality is composed of three loosly coupled components: + * + * 1. {@link NotebookSerializer} enable the editor to open, show, and save notebooks + * 2. {@link NotebookController} own the execution of notebooks, e.g they create output from code cells. + * 3. NotebookRenderer present notebook output in the editor. They run in a separate context. + */ + export namespace notebooks { + + /** + * Creates a new notebook controller. + * + * @param id Identifier of the controller. Must be unique per extension. + * @param notebookType A notebook type for which this controller is for. + * @param label The label of the controller. + * @param handler The execute-handler of the controller. + */ + export function createNotebookController(id: string, notebookType: string, label: string, handler?: (cells: NotebookCell[], notebook: NotebookDocument, controller: NotebookController) => void | Thenable): NotebookController; + + /** + * Register a {@link NotebookCellStatusBarItemProvider cell statusbar item provider} for the given notebook type. + * + * @param notebookType The notebook type to register for. + * @param provider A cell status bar provider. + * @return A {@link Disposable} that unregisters this provider when being disposed. + */ + export function registerNotebookCellStatusBarItemProvider(notebookType: string, provider: NotebookCellStatusBarItemProvider): Disposable; + } + /** * Represents the input box in the Source Control viewlet. */ @@ -11261,7 +12174,7 @@ declare module 'vscode' { interface QuickDiffProvider { /** - * Provide a [uri](#Uri) to the original resource of any given resource uri. + * Provide a {@link Uri} to the original resource of any given resource uri. * * @param uri The uri of the resource open in a text editor. * @param token A cancellation token. @@ -11272,38 +12185,38 @@ declare module 'vscode' { /** * The theme-aware decorations for a - * [source control resource state](#SourceControlResourceState). + * {@link SourceControlResourceState source control resource state}. */ export interface SourceControlResourceThemableDecorations { /** * The icon path for a specific - * [source control resource state](#SourceControlResourceState). + * {@link SourceControlResourceState source control resource state}. */ readonly iconPath?: string | Uri; } /** - * The decorations for a [source control resource state](#SourceControlResourceState). + * The decorations for a {@link SourceControlResourceState source control resource state}. * Can be independently specified for light and dark themes. */ export interface SourceControlResourceDecorations extends SourceControlResourceThemableDecorations { /** - * Whether the [source control resource state](#SourceControlResourceState) should + * Whether the {@link SourceControlResourceState source control resource state} should * be striked-through in the UI. */ readonly strikeThrough?: boolean; /** - * Whether the [source control resource state](#SourceControlResourceState) should + * Whether the {@link SourceControlResourceState source control resource state} should * be faded in the UI. */ readonly faded?: boolean; /** * The title for a specific - * [source control resource state](#SourceControlResourceState). + * {@link SourceControlResourceState source control resource state}. */ readonly tooltip?: string; @@ -11320,23 +12233,23 @@ declare module 'vscode' { /** * An source control resource state represents the state of an underlying workspace - * resource within a certain [source control group](#SourceControlResourceGroup). + * resource within a certain {@link SourceControlResourceGroup source control group}. */ export interface SourceControlResourceState { /** - * The [uri](#Uri) of the underlying resource inside the workspace. + * The {@link Uri} of the underlying resource inside the workspace. */ readonly resourceUri: Uri; /** - * The [command](#Command) which should be run when the resource + * The {@link Command} which should be run when the resource * state is open in the Source Control viewlet. */ readonly command?: Command; /** - * The [decorations](#SourceControlResourceDecorations) for this source control + * The {@link SourceControlResourceDecorations decorations} for this source control * resource state. */ readonly decorations?: SourceControlResourceDecorations; @@ -11364,7 +12277,7 @@ declare module 'vscode' { /** * A source control resource group is a collection of - * [source control resource states](#SourceControlResourceState). + * {@link SourceControlResourceState source control resource states}. */ export interface SourceControlResourceGroup { @@ -11380,13 +12293,13 @@ declare module 'vscode' { /** * Whether this source control resource group is hidden when it contains - * no [source control resource states](#SourceControlResourceState). + * no {@link SourceControlResourceState source control resource states}. */ hideWhenEmpty?: boolean; /** * This group's collection of - * [source control resource states](#SourceControlResourceState). + * {@link SourceControlResourceState source control resource states}. */ resourceStates: SourceControlResourceState[]; @@ -11397,7 +12310,7 @@ declare module 'vscode' { } /** - * An source control is able to provide [resource states](#SourceControlResourceState) + * An source control is able to provide {@link SourceControlResourceState resource states} * to the editor and interact with the editor in several source control related ways. */ export interface SourceControl { @@ -11418,21 +12331,21 @@ declare module 'vscode' { readonly rootUri: Uri | undefined; /** - * The [input box](#SourceControlInputBox) for this source control. + * The {@link SourceControlInputBox input box} for this source control. */ readonly inputBox: SourceControlInputBox; /** - * The UI-visible count of [resource states](#SourceControlResourceState) of + * The UI-visible count of {@link SourceControlResourceState resource states} of * this source control. * - * Equals to the total number of [resource state](#SourceControlResourceState) + * Equals to the total number of {@link SourceControlResourceState resource state} * of this source control, if undefined. */ count?: number; /** - * An optional [quick diff provider](#QuickDiffProvider). + * An optional {@link QuickDiffProvider quick diff provider}. */ quickDiffProvider?: QuickDiffProvider; @@ -11460,7 +12373,7 @@ declare module 'vscode' { statusBarCommands?: Command[]; /** - * Create a new [resource group](#SourceControlResourceGroup). + * Create a new {@link SourceControlResourceGroup resource group}. */ createResourceGroup(id: string, label: string): SourceControlResourceGroup; @@ -11473,7 +12386,7 @@ declare module 'vscode' { export namespace scm { /** - * The [input box](#SourceControlInputBox) for the last source control + * The {@link SourceControlInputBox input box} for the last source control * created by the extension. * * @deprecated Use SourceControl.inputBox instead @@ -11481,12 +12394,12 @@ declare module 'vscode' { export const inputBox: SourceControlInputBox; /** - * Creates a new [source control](#SourceControl) instance. + * Creates a new {@link SourceControl source control} instance. * * @param id An `id` for the source control. Something short, e.g.: `git`. * @param label A human-readable string for the source control. E.g.: `Git`. * @param rootUri An optional Uri of the root of the source control. E.g.: `Uri.parse(workspaceRoot)`. - * @return An instance of [source control](#SourceControl). + * @return An instance of {@link SourceControl source control}. */ export function createSourceControl(id: string, label: string, rootUri?: Uri): SourceControl; } @@ -11548,12 +12461,18 @@ declare module 'vscode' { readonly id: string; /** - * The debug session's type from the [debug configuration](#DebugConfiguration). + * The debug session's type from the {@link DebugConfiguration debug configuration}. */ readonly type: string; /** - * The debug session's name is initially taken from the [debug configuration](#DebugConfiguration). + * The parent session of this debug session, if it was created as a child. + * @see DebugSessionOptions.parentSession + */ + readonly parentSession?: DebugSession; + + /** + * The debug session's name is initially taken from the {@link DebugConfiguration debug configuration}. * Any changes will be properly reflected in the UI. */ name: string; @@ -11564,7 +12483,7 @@ declare module 'vscode' { readonly workspaceFolder: WorkspaceFolder | undefined; /** - * The "resolved" [debug configuration](#DebugConfiguration) of this session. + * The "resolved" {@link DebugConfiguration debug configuration} of this session. * "Resolved" means that * - all variables have been substituted and * - platform specific attribute sections have been "flattened" for the matching platform and removed for non-matching platforms. @@ -11577,21 +12496,21 @@ declare module 'vscode' { customRequest(command: string, args?: any): Thenable; /** - * Maps a VS Code breakpoint to the corresponding Debug Adapter Protocol (DAP) breakpoint that is managed by the debug adapter of the debug session. - * If no DAP breakpoint exists (either because the VS Code breakpoint was not yet registered or because the debug adapter is not interested in the breakpoint), the value `undefined` is returned. + * Maps a breakpoint in the editor to the corresponding Debug Adapter Protocol (DAP) breakpoint that is managed by the debug adapter of the debug session. + * If no DAP breakpoint exists (either because the editor breakpoint was not yet registered or because the debug adapter is not interested in the breakpoint), the value `undefined` is returned. * - * @param breakpoint A VS Code [breakpoint](#Breakpoint). + * @param breakpoint A {@link Breakpoint} in the editor. * @return A promise that resolves to the Debug Adapter Protocol breakpoint or `undefined`. */ getDebugProtocolBreakpoint(breakpoint: Breakpoint): Thenable; } /** - * A custom Debug Adapter Protocol event received from a [debug session](#DebugSession). + * A custom Debug Adapter Protocol event received from a {@link DebugSession debug session}. */ export interface DebugSessionCustomEvent { /** - * The [debug session](#DebugSession) for which the custom event was received. + * The {@link DebugSession debug session} for which the custom event was received. */ readonly session: DebugSession; @@ -11609,28 +12528,28 @@ declare module 'vscode' { /** * A debug configuration provider allows to add debug configurations to the debug service * and to resolve launch configurations before they are used to start a debug session. - * A debug configuration provider is registered via #debug.registerDebugConfigurationProvider. + * A debug configuration provider is registered via {@link debug.registerDebugConfigurationProvider}. */ export interface DebugConfigurationProvider { /** - * Provides [debug configuration](#DebugConfiguration) to the debug service. If more than one debug configuration provider is + * Provides {@link DebugConfiguration debug configuration} to the debug service. If more than one debug configuration provider is * registered for the same type, debug configurations are concatenated in arbitrary order. * * @param folder The workspace folder for which the configurations are used or `undefined` for a folderless setup. * @param token A cancellation token. - * @return An array of [debug configurations](#DebugConfiguration). + * @return An array of {@link DebugConfiguration debug configurations}. */ provideDebugConfigurations?(folder: WorkspaceFolder | undefined, token?: CancellationToken): ProviderResult; /** - * Resolves a [debug configuration](#DebugConfiguration) by filling in missing values or by adding/changing/removing attributes. + * Resolves a {@link DebugConfiguration debug configuration} by filling in missing values or by adding/changing/removing attributes. * If more than one debug configuration provider is registered for the same type, the resolveDebugConfiguration calls are chained * in arbitrary order and the initial debug configuration is piped through the chain. * Returning the value 'undefined' prevents the debug session from starting. * Returning the value 'null' prevents the debug session from starting and opens the underlying debug configuration instead. * * @param folder The workspace folder from which the configuration originates from or `undefined` for a folderless setup. - * @param debugConfiguration The [debug configuration](#DebugConfiguration) to resolve. + * @param debugConfiguration The {@link DebugConfiguration debug configuration} to resolve. * @param token A cancellation token. * @return The resolved debug configuration or undefined or null. */ @@ -11638,14 +12557,14 @@ declare module 'vscode' { /** * This hook is directly called after 'resolveDebugConfiguration' but with all variables substituted. - * It can be used to resolve or verify a [debug configuration](#DebugConfiguration) by filling in missing values or by adding/changing/removing attributes. + * It can be used to resolve or verify a {@link DebugConfiguration debug configuration} by filling in missing values or by adding/changing/removing attributes. * If more than one debug configuration provider is registered for the same type, the 'resolveDebugConfigurationWithSubstitutedVariables' calls are chained * in arbitrary order and the initial debug configuration is piped through the chain. * Returning the value 'undefined' prevents the debug session from starting. * Returning the value 'null' prevents the debug session from starting and opens the underlying debug configuration instead. * * @param folder The workspace folder from which the configuration originates from or `undefined` for a folderless setup. - * @param debugConfiguration The [debug configuration](#DebugConfiguration) to resolve. + * @param debugConfiguration The {@link DebugConfiguration debug configuration} to resolve. * @param token A cancellation token. * @return The resolved debug configuration or undefined or null. */ @@ -11669,7 +12588,7 @@ declare module 'vscode' { /** * The command or path of the debug adapter executable. * A command must be either an absolute path of an executable or the name of an command to be looked up via the PATH environment variable. - * The special value 'node' will be mapped to VS Code's built-in Node.js runtime. + * The special value 'node' will be mapped to the editor's built-in Node.js runtime. */ readonly command: string; @@ -11740,12 +12659,12 @@ declare module 'vscode' { } /** - * A debug adapter that implements the Debug Adapter Protocol can be registered with VS Code if it implements the DebugAdapter interface. + * A debug adapter that implements the Debug Adapter Protocol can be registered with the editor if it implements the DebugAdapter interface. */ export interface DebugAdapter extends Disposable { /** - * An event which fires after the debug adapter has sent a Debug Adapter Protocol message to VS Code. + * An event which fires after the debug adapter has sent a Debug Adapter Protocol message to the editor. * Messages can be requests, responses, or events. */ readonly onDidSendMessage: Event; @@ -11775,10 +12694,10 @@ declare module 'vscode' { export interface DebugAdapterDescriptorFactory { /** * 'createDebugAdapterDescriptor' is called at the start of a debug session to provide details about the debug adapter to use. - * These details must be returned as objects of type [DebugAdapterDescriptor](#DebugAdapterDescriptor). + * These details must be returned as objects of type {@link DebugAdapterDescriptor}. * Currently two types of debug adapters are supported: - * - a debug adapter executable is specified as a command path and arguments (see [DebugAdapterExecutable](#DebugAdapterExecutable)), - * - a debug adapter server reachable via a communication port (see [DebugAdapterServer](#DebugAdapterServer)). + * - a debug adapter executable is specified as a command path and arguments (see {@link DebugAdapterExecutable}), + * - a debug adapter server reachable via a communication port (see {@link DebugAdapterServer}). * If the method is not implemented the default behavior is this: * createDebugAdapter(session: DebugSession, executable: DebugAdapterExecutable) { * if (typeof session.configuration.debugServer === 'number') { @@ -11786,15 +12705,15 @@ declare module 'vscode' { * } * return executable; * } - * @param session The [debug session](#DebugSession) for which the debug adapter will be used. + * @param session The {@link DebugSession debug session} for which the debug adapter will be used. * @param executable The debug adapter's executable information as specified in the package.json (or undefined if no such information exists). - * @return a [debug adapter descriptor](#DebugAdapterDescriptor) or undefined. + * @return a {@link DebugAdapterDescriptor debug adapter descriptor} or undefined. */ createDebugAdapterDescriptor(session: DebugSession, executable: DebugAdapterExecutable | undefined): ProviderResult; } /** - * A Debug Adapter Tracker is a means to track the communication between VS Code and a Debug Adapter. + * A Debug Adapter Tracker is a means to track the communication between the editor and a Debug Adapter. */ export interface DebugAdapterTracker { /** @@ -11802,11 +12721,11 @@ declare module 'vscode' { */ onWillStartSession?(): void; /** - * The debug adapter is about to receive a Debug Adapter Protocol message from VS Code. + * The debug adapter is about to receive a Debug Adapter Protocol message from the editor. */ onWillReceiveMessage?(message: any): void; /** - * The debug adapter has sent a Debug Adapter Protocol message to VS Code. + * The debug adapter has sent a Debug Adapter Protocol message to the editor. */ onDidSendMessage?(message: any): void; /** @@ -11826,10 +12745,10 @@ declare module 'vscode' { export interface DebugAdapterTrackerFactory { /** * The method 'createDebugAdapterTracker' is called at the start of a debug session in order - * to return a "tracker" object that provides read-access to the communication between VS Code and a debug adapter. + * to return a "tracker" object that provides read-access to the communication between the editor and a debug adapter. * - * @param session The [debug session](#DebugSession) for which the debug adapter tracker will be used. - * @return A [debug adapter tracker](#DebugAdapterTracker) or undefined. + * @param session The {@link DebugSession debug session} for which the debug adapter tracker will be used. + * @return A {@link DebugAdapterTracker debug adapter tracker} or undefined. */ createDebugAdapterTracker(session: DebugSession): ProviderResult; } @@ -11855,23 +12774,23 @@ declare module 'vscode' { } /** - * An event describing the changes to the set of [breakpoints](#Breakpoint). + * An event describing the changes to the set of {@link Breakpoint breakpoints}. */ export interface BreakpointsChangeEvent { /** * Added breakpoints. */ - readonly added: ReadonlyArray; + readonly added: readonly Breakpoint[]; /** * Removed breakpoints. */ - readonly removed: ReadonlyArray; + readonly removed: readonly Breakpoint[]; /** * Changed breakpoints. */ - readonly changed: ReadonlyArray; + readonly changed: readonly Breakpoint[]; } /** @@ -11933,7 +12852,7 @@ declare module 'vscode' { } /** - * Debug console mode used by debug session, see [options](#DebugSessionOptions). + * Debug console mode used by debug session, see {@link DebugSessionOptions options}. */ export enum DebugConsoleMode { /** @@ -11949,7 +12868,7 @@ declare module 'vscode' { } /** - * Options for [starting a debug session](#debug.startDebugging). + * Options for {@link debug.startDebugging starting a debug session}. */ export interface DebugSessionOptions { @@ -11984,7 +12903,7 @@ declare module 'vscode' { * A DebugConfigurationProviderTriggerKind specifies when the `provideDebugConfigurations` method of a `DebugConfigurationProvider` is triggered. * Currently there are two situations: to provide the initial debug configurations for a newly created launch.json or * to provide dynamically generated debug configurations when the user asks for them through the UI (e.g. via the "Select and Start Debugging" command). - * A trigger kind is used when registering a `DebugConfigurationProvider` with #debug.registerDebugConfigurationProvider. + * A trigger kind is used when registering a `DebugConfigurationProvider` with {@link debug.registerDebugConfigurationProvider}. */ export enum DebugConfigurationProviderTriggerKind { /** @@ -12003,14 +12922,14 @@ declare module 'vscode' { export namespace debug { /** - * The currently active [debug session](#DebugSession) or `undefined`. The active debug session is the one + * The currently active {@link DebugSession debug session} or `undefined`. The active debug session is the one * represented by the debug action floating window or the one currently shown in the drop down menu of the debug action floating window. * If no debug session is active, the value is `undefined`. */ export let activeDebugSession: DebugSession | undefined; /** - * The currently active [debug console](#DebugConsole). + * The currently active {@link DebugConsole debug console}. * If no debug session is active, output sent to the debug console is not shown. */ export let activeDebugConsole: DebugConsole; @@ -12021,35 +12940,35 @@ declare module 'vscode' { export let breakpoints: Breakpoint[]; /** - * An [event](#Event) which fires when the [active debug session](#debug.activeDebugSession) + * An {@link Event} which fires when the {@link debug.activeDebugSession active debug session} * has changed. *Note* that the event also fires when the active debug session changes * to `undefined`. */ export const onDidChangeActiveDebugSession: Event; /** - * An [event](#Event) which fires when a new [debug session](#DebugSession) has been started. + * An {@link Event} which fires when a new {@link DebugSession debug session} has been started. */ export const onDidStartDebugSession: Event; /** - * An [event](#Event) which fires when a custom DAP event is received from the [debug session](#DebugSession). + * An {@link Event} which fires when a custom DAP event is received from the {@link DebugSession debug session}. */ export const onDidReceiveDebugSessionCustomEvent: Event; /** - * An [event](#Event) which fires when a [debug session](#DebugSession) has terminated. + * An {@link Event} which fires when a {@link DebugSession debug session} has terminated. */ export const onDidTerminateDebugSession: Event; /** - * An [event](#Event) that is emitted when the set of breakpoints is added, removed, or changed. + * An {@link Event} that is emitted when the set of breakpoints is added, removed, or changed. */ export const onDidChangeBreakpoints: Event; /** - * Register a [debug configuration provider](#DebugConfigurationProvider) for a specific debug type. - * The optional [triggerKind](#DebugConfigurationProviderTriggerKind) can be used to specify when the `provideDebugConfigurations` method of the provider is triggered. + * Register a {@link DebugConfigurationProvider debug configuration provider} for a specific debug type. + * The optional {@link DebugConfigurationProviderTriggerKind triggerKind} can be used to specify when the `provideDebugConfigurations` method of the provider is triggered. * Currently two trigger kinds are possible: with the value `Initial` (or if no trigger kind argument is given) the `provideDebugConfigurations` method is used to provide the initial debug configurations to be copied into a newly created launch.json. * With the trigger kind `Dynamic` the `provideDebugConfigurations` method is used to dynamically determine debug configurations to be presented to the user (in addition to the static configurations from the launch.json). * Please note that the `triggerKind` argument only applies to the `provideDebugConfigurations` method: so the `resolveDebugConfiguration` methods are not affected at all. @@ -12057,20 +12976,20 @@ declare module 'vscode' { * More than one provider can be registered for the same type. * * @param type The debug type for which the provider is registered. - * @param provider The [debug configuration provider](#DebugConfigurationProvider) to register. - * @param triggerKind The [trigger](#DebugConfigurationProviderTrigger) for which the 'provideDebugConfiguration' method of the provider is registered. If `triggerKind` is missing, the value `DebugConfigurationProviderTriggerKind.Initial` is assumed. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @param provider The {@link DebugConfigurationProvider debug configuration provider} to register. + * @param triggerKind The {@link DebugConfigurationProviderTrigger trigger} for which the 'provideDebugConfiguration' method of the provider is registered. If `triggerKind` is missing, the value `DebugConfigurationProviderTriggerKind.Initial` is assumed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerDebugConfigurationProvider(debugType: string, provider: DebugConfigurationProvider, triggerKind?: DebugConfigurationProviderTriggerKind): Disposable; /** - * Register a [debug adapter descriptor factory](#DebugAdapterDescriptorFactory) for a specific debug type. + * Register a {@link DebugAdapterDescriptorFactory debug adapter descriptor factory} for a specific debug type. * An extension is only allowed to register a DebugAdapterDescriptorFactory for the debug type(s) defined by the extension. Otherwise an error is thrown. * Registering more than one DebugAdapterDescriptorFactory for a debug type results in an error. * * @param debugType The debug type for which the factory is registered. - * @param factory The [debug adapter descriptor factory](#DebugAdapterDescriptorFactory) to register. - * @return A [disposable](#Disposable) that unregisters this factory when being disposed. + * @param factory The {@link DebugAdapterDescriptorFactory debug adapter descriptor factory} to register. + * @return A {@link Disposable} that unregisters this factory when being disposed. */ export function registerDebugAdapterDescriptorFactory(debugType: string, factory: DebugAdapterDescriptorFactory): Disposable; @@ -12078,27 +12997,27 @@ declare module 'vscode' { * Register a debug adapter tracker factory for the given debug type. * * @param debugType The debug type for which the factory is registered or '*' for matching all debug types. - * @param factory The [debug adapter tracker factory](#DebugAdapterTrackerFactory) to register. - * @return A [disposable](#Disposable) that unregisters this factory when being disposed. + * @param factory The {@link DebugAdapterTrackerFactory debug adapter tracker factory} to register. + * @return A {@link Disposable} that unregisters this factory when being disposed. */ export function registerDebugAdapterTrackerFactory(debugType: string, factory: DebugAdapterTrackerFactory): Disposable; /** * Start debugging by using either a named launch or named compound configuration, - * or by directly passing a [DebugConfiguration](#DebugConfiguration). + * or by directly passing a {@link DebugConfiguration}. * The named configurations are looked up in '.vscode/launch.json' found in the given folder. * Before debugging starts, all unsaved files are saved and the launch configurations are brought up-to-date. * Folder specific variables used in the configuration (e.g. '${workspaceFolder}') are resolved against the given folder. - * @param folder The [workspace folder](#WorkspaceFolder) for looking up named configurations and resolving variables or `undefined` for a non-folder setup. - * @param nameOrConfiguration Either the name of a debug or compound configuration or a [DebugConfiguration](#DebugConfiguration) object. - * @param parentSessionOrOptions Debug session options. When passed a parent [debug session](#DebugSession), assumes options with just this parent session. + * @param folder The {@link WorkspaceFolder workspace folder} for looking up named configurations and resolving variables or `undefined` for a non-folder setup. + * @param nameOrConfiguration Either the name of a debug or compound configuration or a {@link DebugConfiguration} object. + * @param parentSessionOrOptions Debug session options. When passed a parent {@link DebugSession debug session}, assumes options with just this parent session. * @return A thenable that resolves when debugging could be successfully started. */ export function startDebugging(folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration, parentSessionOrOptions?: DebugSession | DebugSessionOptions): Thenable; /** * Stop the given debug session or stop all debug sessions if session is omitted. - * @param session The [debug session](#DebugSession) to stop; if omitted all sessions are stopped. + * @param session The {@link DebugSession debug session} to stop; if omitted all sessions are stopped. */ export function stopDebugging(session?: DebugSession): Thenable; @@ -12106,18 +13025,18 @@ declare module 'vscode' { * Add breakpoints. * @param breakpoints The breakpoints to add. */ - export function addBreakpoints(breakpoints: Breakpoint[]): void; + export function addBreakpoints(breakpoints: readonly Breakpoint[]): void; /** * Remove breakpoints. * @param breakpoints The breakpoints to remove. */ - export function removeBreakpoints(breakpoints: Breakpoint[]): void; + export function removeBreakpoints(breakpoints: readonly Breakpoint[]): void; /** * Converts a "Source" descriptor object received via the Debug Adapter Protocol into a Uri that can be used to load its contents. * If the source descriptor is based on a path, a file Uri is returned. - * If the source descriptor uses a reference number, a specific debug Uri (scheme 'debug') is constructed that requires a corresponding VS Code ContentProvider and a running debug session + * If the source descriptor uses a reference number, a specific debug Uri (scheme 'debug') is constructed that requires a corresponding ContentProvider and a running debug session * * If the "Source" descriptor has insufficient information for creating the Uri, an error is thrown. * @@ -12130,7 +13049,7 @@ declare module 'vscode' { /** * Namespace for dealing with installed extensions. Extensions are represented - * by an [extension](#Extension)-interface which enables reflection on them. + * by an {@link Extension}-interface which enables reflection on them. * * Extension writers can provide APIs to other extensions by returning their API public * surface from the `activate`-call. @@ -12150,8 +13069,8 @@ declare module 'vscode' { * } * ``` * When depending on the API of another extension add an `extensionDependencies`-entry - * to `package.json`, and use the [getExtension](#extensions.getExtension)-function - * and the [exports](#Extension.exports)-property, like below: + * to `package.json`, and use the {@link extensions.getExtension getExtension}-function + * and the {@link Extension.exports exports}-property, like below: * * ```javascript * let mathExt = extensions.getExtension('genius.math'); @@ -12181,7 +13100,7 @@ declare module 'vscode' { /** * All extensions currently known to the system. */ - export const all: ReadonlyArray>; + export const all: readonly Extension[]; /** * An event which fires when `extensions.all` changes. This can happen when extensions are @@ -12191,7 +13110,7 @@ declare module 'vscode' { } /** - * Collapsible state of a [comment thread](#CommentThread) + * Collapsible state of a {@link CommentThread comment thread} */ export enum CommentThreadCollapsibleState { /** @@ -12206,7 +13125,7 @@ declare module 'vscode' { } /** - * Comment mode of a [comment](#Comment) + * Comment mode of a {@link Comment} */ export enum CommentMode { /** @@ -12221,7 +13140,7 @@ declare module 'vscode' { } /** - * A collection of [comments](#Comment) representing a conversation at a particular range in a document. + * A collection of {@link Comment comments} representing a conversation at a particular range in a document. */ export interface CommentThread { /** @@ -12238,7 +13157,7 @@ declare module 'vscode' { /** * The ordered comments of the thread. */ - comments: ReadonlyArray; + comments: readonly Comment[]; /** * Whether the thread should be collapsed or expanded when opening the document. @@ -12273,7 +13192,7 @@ declare module 'vscode' { contextValue?: string; /** - * The optional human-readable label describing the [Comment Thread](#CommentThread) + * The optional human-readable label describing the {@link CommentThread Comment Thread} */ label?: string; @@ -12286,7 +13205,7 @@ declare module 'vscode' { } /** - * Author information of a [comment](#Comment) + * Author information of a {@link Comment} */ export interface CommentAuthorInformation { /** @@ -12301,7 +13220,7 @@ declare module 'vscode' { } /** - * Reactions of a [comment](#Comment) + * Reactions of a {@link Comment} */ export interface CommentReaction { /** @@ -12335,12 +13254,12 @@ declare module 'vscode' { body: string | MarkdownString; /** - * [Comment mode](#CommentMode) of the comment + * {@link CommentMode Comment mode} of the comment */ mode: CommentMode; /** - * The [author information](#CommentAuthorInformation) of the comment + * The {@link CommentAuthorInformation author information} of the comment */ author: CommentAuthorInformation; @@ -12365,12 +13284,12 @@ declare module 'vscode' { contextValue?: string; /** - * Optional reactions of the [comment](#Comment) + * Optional reactions of the {@link Comment} */ reactions?: CommentReaction[]; /** - * Optional label describing the [Comment](#Comment) + * Optional label describing the {@link Comment} * Label will be rendered next to authorName if exists. */ label?: string; @@ -12381,7 +13300,7 @@ declare module 'vscode' { */ export interface CommentReply { /** - * The active [comment thread](#CommentThread) + * The active {@link CommentThread comment thread} */ thread: CommentThread; @@ -12392,7 +13311,7 @@ declare module 'vscode' { } /** - * Commenting range provider for a [comment controller](#CommentController). + * Commenting range provider for a {@link CommentController comment controller}. */ export interface CommentingRangeProvider { /** @@ -12402,7 +13321,7 @@ declare module 'vscode' { } /** - * Represents a [comment controller](#CommentController)'s [options](#CommentController.options). + * Represents a {@link CommentController comment controller}'s {@link CommentController.options options}. */ export interface CommentOptions { /** @@ -12417,7 +13336,7 @@ declare module 'vscode' { } /** - * A comment controller is able to provide [comments](#CommentThread) support to the editor and + * A comment controller is able to provide {@link CommentThread comments} support to the editor and * provide users various ways to interact with comments. */ export interface CommentController { @@ -12437,31 +13356,31 @@ declare module 'vscode' { options?: CommentOptions; /** - * Optional commenting range provider. Provide a list [ranges](#Range) which support commenting to any given resource uri. + * Optional commenting range provider. Provide a list {@link Range ranges} which support commenting to any given resource uri. * * If not provided, users can leave comments in any document opened in the editor. */ commentingRangeProvider?: CommentingRangeProvider; /** - * Create a [comment thread](#CommentThread). The comment thread will be displayed in visible text editors (if the resource matches) + * Create a {@link CommentThread comment thread}. The comment thread will be displayed in visible text editors (if the resource matches) * and Comments Panel once created. * * @param uri The uri of the document the thread has been created on. * @param range The range the comment thread is located within the document. * @param comments The ordered comments of the thread. */ - createCommentThread(uri: Uri, range: Range, comments: Comment[]): CommentThread; + createCommentThread(uri: Uri, range: Range, comments: readonly Comment[]): CommentThread; /** - * Optional reaction handler for creating and deleting reactions on a [comment](#Comment). + * Optional reaction handler for creating and deleting reactions on a {@link Comment}. */ reactionHandler?: (comment: Comment, reaction: CommentReaction) => Thenable; /** * Dispose this comment controller. * - * Once disposed, all [comment threads](#CommentThread) created by this comment controller will also be removed from the editor + * Once disposed, all {@link CommentThread comment threads} created by this comment controller will also be removed from the editor * and Comments Panel. */ dispose(): void; @@ -12469,11 +13388,11 @@ declare module 'vscode' { namespace comments { /** - * Creates a new [comment controller](#CommentController) instance. + * Creates a new {@link CommentController comment controller} instance. * * @param id An `id` for the comment controller. * @param label A human-readable string for the comment controller. - * @return An instance of [comment controller](#CommentController). + * @return An instance of {@link CommentController comment controller}. */ export function createCommentController(id: string, label: string): CommentController; } @@ -12501,13 +13420,13 @@ declare module 'vscode' { /** * The permissions granted by the session's access token. Available scopes - * are defined by the [AuthenticationProvider](#AuthenticationProvider). + * are defined by the {@link AuthenticationProvider}. */ - readonly scopes: ReadonlyArray; + readonly scopes: readonly string[]; } /** - * The information of an account associated with an [AuthenticationSession](#AuthenticationSession). + * The information of an account associated with an {@link AuthenticationSession}. */ export interface AuthenticationSessionAccountInformation { /** @@ -12523,7 +13442,7 @@ declare module 'vscode' { /** - * Options to be used when getting an [AuthenticationSession](#AuthenticationSession) from an [AuthenticationProvider](#AuthenticationProvider). + * Options to be used when getting an {@link AuthenticationSession} from an {@link AuthenticationProvider}. */ export interface AuthenticationGetSessionOptions { /** @@ -12544,8 +13463,8 @@ declare module 'vscode' { * Whether the existing user session preference should be cleared. * * For authentication providers that support being signed into multiple accounts at once, the user will be - * prompted to select an account to use when [getSession](#authentication.getSession) is called. This preference - * is remembered until [getSession](#authentication.getSession) is called with this flag. + * prompted to select an account to use when {@link authentication.getSession getSession} is called. This preference + * is remembered until {@link authentication.getSession getSession} is called with this flag. * * Defaults to false. */ @@ -12553,7 +13472,7 @@ declare module 'vscode' { } /** - * Basic information about an [authenticationProvider](#AuthenticationProvider) + * Basic information about an {@link AuthenticationProvider} */ export interface AuthenticationProviderInformation { /** @@ -12568,17 +13487,17 @@ declare module 'vscode' { } /** - * An [event](#Event) which fires when an [AuthenticationSession](#AuthenticationSession) is added, removed, or changed. + * An {@link Event} which fires when an {@link AuthenticationSession} is added, removed, or changed. */ export interface AuthenticationSessionsChangeEvent { /** - * The [authenticationProvider](#AuthenticationProvider) that has had its sessions change. + * The {@link AuthenticationProvider} that has had its sessions change. */ readonly provider: AuthenticationProviderInformation; } /** - * Options for creating an [AuthenticationProvider](#AuthenticationProvider). + * Options for creating an {@link AuthenticationProvider}. */ export interface AuthenticationProviderOptions { /** @@ -12589,25 +13508,25 @@ declare module 'vscode' { } /** - * An [event](#Event) which fires when an [AuthenticationSession](#AuthenticationSession) is added, removed, or changed. + * An {@link Event} which fires when an {@link AuthenticationSession} is added, removed, or changed. */ export interface AuthenticationProviderAuthenticationSessionsChangeEvent { /** - * The [AuthenticationSession](#AuthenticationSession)s of the [AuthenticationProvider](#AuthentiationProvider) that have been added. + * The {@link AuthenticationSession}s of the {@link AuthentiationProvider AuthenticationProvider} that have been added. */ - readonly added?: ReadonlyArray; + readonly added?: readonly AuthenticationSession[]; /** - * The [AuthenticationSession](#AuthenticationSession)s of the [AuthenticationProvider](#AuthentiationProvider) that have been removed. + * The {@link AuthenticationSession}s of the {@link AuthentiationProvider AuthenticationProvider} that have been removed. */ - readonly removed?: ReadonlyArray; + readonly removed?: readonly AuthenticationSession[]; /** - * The [AuthenticationSession](#AuthenticationSession)s of the [AuthenticationProvider](#AuthentiationProvider) that have been changed. + * The {@link AuthenticationSession}s of the {@link AuthentiationProvider AuthenticationProvider} that have been changed. * A session changes when its data excluding the id are updated. An example of this is a session refresh that results in a new * access token being set for the session. */ - readonly changed?: ReadonlyArray; + readonly changed?: readonly AuthenticationSession[]; } /** @@ -12615,7 +13534,7 @@ declare module 'vscode' { */ export interface AuthenticationProvider { /** - * An [event](#Event) which fires when the array of sessions has changed, or data + * An {@link Event} which fires when the array of sessions has changed, or data * within a session has changed. */ readonly onDidChangeSessions: Event; @@ -12626,7 +13545,7 @@ declare module 'vscode' { * these permissions, otherwise all sessions should be returned. * @returns A promise that resolves to an array of authentication sessions. */ - getSessions(scopes?: string[]): Thenable>; + getSessions(scopes?: readonly string[]): Thenable; /** * Prompts a user to login. @@ -12641,7 +13560,7 @@ declare module 'vscode' { * @param scopes A list of scopes, permissions, that the new session should be created with. * @returns A promise that resolves to an authentication session. */ - createSession(scopes: string[]): Thenable; + createSession(scopes: readonly string[]): Thenable; /** * Removes the session corresponding to session id. @@ -12666,13 +13585,13 @@ declare module 'vscode' { * quickpick to select which account they would like to use. * * Currently, there are only two authentication providers that are contributed from built in extensions - * to VS Code that implement GitHub and Microsoft authentication: their providerId's are 'github' and 'microsoft'. + * to the editor that implement GitHub and Microsoft authentication: their providerId's are 'github' and 'microsoft'. * @param providerId The id of the provider to use * @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication provider - * @param options The [getSessionOptions](#GetSessionOptions) to use + * @param options The {@link GetSessionOptions} to use * @returns A thenable that resolves to an authentication session */ - export function getSession(providerId: string, scopes: string[], options: AuthenticationGetSessionOptions & { createIfNone: true }): Thenable; + export function getSession(providerId: string, scopes: readonly string[], options: AuthenticationGetSessionOptions & { createIfNone: true }): Thenable; /** * Get an authentication session matching the desired scopes. Rejects if a provider with providerId is not @@ -12681,16 +13600,16 @@ declare module 'vscode' { * quickpick to select which account they would like to use. * * Currently, there are only two authentication providers that are contributed from built in extensions - * to VS Code that implement GitHub and Microsoft authentication: their providerId's are 'github' and 'microsoft'. + * to the editor that implement GitHub and Microsoft authentication: their providerId's are 'github' and 'microsoft'. * @param providerId The id of the provider to use * @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication provider - * @param options The [getSessionOptions](#GetSessionOptions) to use + * @param options The {@link GetSessionOptions} to use * @returns A thenable that resolves to an authentication session if available, or undefined if there are no sessions */ - export function getSession(providerId: string, scopes: string[], options?: AuthenticationGetSessionOptions): Thenable; + export function getSession(providerId: string, scopes: readonly string[], options?: AuthenticationGetSessionOptions): Thenable; /** - * An [event](#Event) which fires when the authentication sessions of an authentication provider have + * An {@link Event} which fires when the authentication sessions of an authentication provider have * been added, removed, or changed. */ export const onDidChangeSessions: Event; @@ -12705,7 +13624,7 @@ declare module 'vscode' { * @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. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerAuthenticationProvider(id: string, label: string, provider: AuthenticationProvider, options?: AuthenticationProviderOptions): Disposable; } diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 52a4e17b77..ed0b52f07f 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -9,7 +9,7 @@ * distribution and CANNOT be used in published extensions. * * To test these API in local environment: - * - Use Insiders release of VS Code. + * - Use Insiders release of 'VS Code'. * - Add `"enableProposedApi": true` to your package.json. * - Copy this file to your project. */ @@ -19,16 +19,16 @@ declare module 'vscode' { //#region auth provider: https://github.com/microsoft/vscode/issues/88309 /** - * An [event](#Event) which fires when an [AuthenticationProvider](#AuthenticationProvider) is added or removed. + * An {@link Event} which fires when an {@link AuthenticationProvider} is added or removed. */ export interface AuthenticationProvidersChangeEvent { /** - * The ids of the [authenticationProvider](#AuthenticationProvider)s that have been added. + * The ids of the {@link AuthenticationProvider}s that have been added. */ readonly added: ReadonlyArray; /** - * The ids of the [authenticationProvider](#AuthenticationProvider)s that have been removed. + * The ids of the {@link AuthenticationProvider}s that have been removed. */ readonly removed: ReadonlyArray; } @@ -80,16 +80,10 @@ declare module 'vscode' { constructor(host: string, port: number, connectionToken?: string); } - export enum RemoteTrustOption { - Unknown = 0, - DisableTrust = 1, - MachineTrusted = 2 - } - export interface ResolvedOptions { extensionHostEnv?: { [key: string]: string | null; }; - trust?: RemoteTrustOption; + isTrusted?: boolean; } export interface TunnelOptions { @@ -150,7 +144,24 @@ declare module 'vscode' { } export interface RemoteAuthorityResolver { + /** + * Resolve the authority part of the current opened `vscode-remote://` URI. + * + * This method will be invoked once during the startup of the editor and again each time + * the editor detects a disconnection. + * + * @param authority The authority part of the current opened `vscode-remote://` URI. + * @param context A context indicating if this is the first call or a subsequent call. + */ resolve(authority: string, context: RemoteAuthorityResolverContext): ResolverResult | Thenable; + + /** + * Get the canonical URI (if applicable) for a `vscode-remote://` URI. + * + * @returns The canonical URI or undefined if the uri is already canonical. + */ + getCanonicalURI?(uri: Uri): ProviderResult; + /** * Can be optionally implemented if the extension can forward ports better than the core. * When not implemented, the core will use its default forwarding logic. @@ -289,7 +300,7 @@ declare module 'vscode' { /** * A file glob pattern to match file paths against. * TODO@roblourens merge this with the GlobPattern docs/definition in vscode.d.ts. - * @see [GlobPattern](#GlobPattern) + * @see {@link GlobPattern} */ export type GlobString = string; @@ -392,13 +403,32 @@ declare module 'vscode' { Warning = 2, } + /** + * A message regarding a completed search. + */ + export interface TextSearchCompleteMessage { + /** + * Markdown text of the message. + */ + text: string, + /** + * Whether the source of the message is trusted, command links are disabled for untrusted message sources. + * Messaged are untrusted by default. + */ + trusted?: boolean, + /** + * The message type, this affects how the message will be rendered. + */ + type: TextSearchCompleteMessageType, + } + /** * Information collected when text search is complete. */ export interface TextSearchComplete { /** * Whether the search hit the limit on the maximum number of search results. - * `maxResults` on [`TextSearchOptions`](#TextSearchOptions) specifies the max number of results. + * `maxResults` on {@link TextSearchOptions `TextSearchOptions`} specifies the max number of results. * - If exactly that number of matches exist, this should be false. * - If `maxResults` matches are returned and more exist, this should be true. * - If search hits an internal limit which is less than `maxResults`, this should be true. @@ -408,11 +438,13 @@ declare module 'vscode' { /** * Additional information regarding the state of the completed search. * - * Messages with "Information" tyle support links in markdown syntax: + * Messages with "Information" style support links in markdown syntax: * - Click to [run a command](command:workbench.action.OpenQuickPick) * - Click to [open a website](https://aka.ms) + * + * Commands may optionally return { triggerSearch: true } to signal to the editor that the original search should run be again. */ - message?: { text: string, type: TextSearchCompleteMessageType } | { text: string, type: TextSearchCompleteMessageType }[]; + message?: TextSearchCompleteMessage | TextSearchCompleteMessage[]; } /** @@ -521,7 +553,7 @@ declare module 'vscode' { /** * A FileSearchProvider provides search results for files in the given folder that match a query string. It can be invoked by quickopen or other extensions. * - * A FileSearchProvider is the more powerful of two ways to implement file search in VS Code. Use a FileSearchProvider if you wish to search within a folder for + * A FileSearchProvider is the more powerful of two ways to implement file search in the editor. Use a FileSearchProvider if you wish to search within a folder for * all files that match the user's query. * * The FileSearchProvider will be invoked on every keypress in quickopen. When `workspace.findFiles` is called, it will be invoked with an empty query string, @@ -545,7 +577,7 @@ declare module 'vscode' { * * @param scheme The provider will be invoked for workspace folders that have this file scheme. * @param provider The provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerFileSearchProvider(scheme: string, provider: FileSearchProvider): Disposable; @@ -556,7 +588,7 @@ declare module 'vscode' { * * @param scheme The provider will be invoked for workspace folders that have this file scheme. * @param provider The provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerTextSearchProvider(scheme: string, provider: TextSearchProvider): Disposable; } @@ -570,14 +602,14 @@ declare module 'vscode' { */ export interface FindTextInFilesOptions { /** - * A [glob pattern](#GlobPattern) that defines the files to search for. The glob pattern - * will be matched against the file paths of files relative to their workspace. Use a [relative pattern](#RelativePattern) - * to restrict the search results to a [workspace folder](#WorkspaceFolder). + * A {@link GlobPattern glob pattern} that defines the files to search for. The glob pattern + * will be matched against the file paths of files relative to their workspace. Use a {@link RelativePattern relative pattern} + * to restrict the search results to a {@link WorkspaceFolder workspace folder}. */ include?: GlobPattern; /** - * A [glob pattern](#GlobPattern) that defines files and folders to exclude. The glob pattern + * A {@link GlobPattern glob pattern} that defines files and folders to exclude. The glob pattern * will be matched against the file paths of resulting matches relative to their workspace. When `undefined`, default excludes will * apply. */ @@ -635,7 +667,7 @@ declare module 'vscode' { export namespace workspace { /** - * Search text in files across all [workspace folders](#workspace.workspaceFolders) in the workspace. + * Search text in files across all {@link workspace.workspaceFolders workspace folders} in the workspace. * @param query The query parameters for the search - the search string, whether it's case-sensitive, or a regex, or matches whole words. * @param callback A callback, called for each result * @param token A token that can be used to signal cancellation to the underlying search engine. @@ -644,7 +676,7 @@ declare module 'vscode' { export function findTextInFiles(query: TextSearchQuery, callback: (result: TextSearchResult) => void, token?: CancellationToken): Thenable; /** - * Search text in files across all [workspace folders](#workspace.workspaceFolders) in the workspace. + * Search text in files across all {@link workspace.workspaceFolders workspace folders} in the workspace. * @param query The query parameters for the search - the search string, whether it's case-sensitive, or a regex, or matches whole words. * @param options An optional set of query options. Include and exclude patterns, maxResults, etc. * @param callback A callback, called for each result @@ -674,13 +706,13 @@ declare module 'vscode' { * Registers a diff information command that can be invoked via a keyboard shortcut, * a menu item, an action, or directly. * - * Diff information commands are different from ordinary [commands](#commands.registerCommand) as + * Diff information commands are different from ordinary {@link commands.registerCommand commands} as * they only execute when there is an active diff editor when the command is called, and the diff * information has been computed. Also, the command handler of an editor command has access to * the diff information. * * @param command A unique identifier for the command. - * @param callback A command handler function with access to the [diff information](#LineChange). + * @param callback A command handler function with access to the {@link LineChange diff information}. * @param thisArg The `this` context used when invoking the handler function. * @return Disposable which unregisters this command on disposal. */ @@ -788,7 +820,7 @@ declare module 'vscode' { export interface TerminalDataWriteEvent { /** - * The [terminal](#Terminal) for which the data was written. + * The {@link Terminal} for which the data was written. */ readonly terminal: Terminal; /** @@ -811,22 +843,22 @@ declare module 'vscode' { //#region Terminal dimensions property and change event https://github.com/microsoft/vscode/issues/55718 /** - * An [event](#Event) which fires when a [Terminal](#Terminal)'s dimensions change. + * An {@link Event} which fires when a {@link Terminal}'s dimensions change. */ export interface TerminalDimensionsChangeEvent { /** - * The [terminal](#Terminal) for which the dimensions have changed. + * The {@link Terminal} for which the dimensions have changed. */ readonly terminal: Terminal; /** - * The new value for the [terminal's dimensions](#Terminal.dimensions). + * The new value for the {@link Terminal.dimensions terminal's dimensions}. */ readonly dimensions: TerminalDimensions; } export namespace window { /** - * An event which fires when the [dimensions](#Terminal.dimensions) of the terminal change. + * An event which fires when the {@link Terminal.dimensions dimensions} of the terminal change. */ export const onDidChangeTerminalDimensions: Event; } @@ -842,15 +874,26 @@ declare module 'vscode' { //#endregion - //#region Terminal initial text https://github.com/microsoft/vscode/issues/120368 + //#region Terminal name change event https://github.com/microsoft/vscode/issues/114898 - export interface TerminalOptions { + export interface Pseudoterminal { /** - * A message to write to the terminal on first launch, note that this is not sent to the - * process but, rather written directly to the terminal. This supports escape sequences such - * a setting text style. + * An event that when fired allows changing the name of the terminal. + * + * **Example:** Change the terminal name to "My new terminal". + * ```typescript + * const writeEmitter = new vscode.EventEmitter(); + * const changeNameEmitter = new vscode.EventEmitter(); + * const pty: vscode.Pseudoterminal = { + * onDidWrite: writeEmitter.event, + * onDidChangeName: changeNameEmitter.event, + * open: () => changeNameEmitter.fire('My new terminal'), + * close: () => {} + * }; + * vscode.window.createTerminal({ name: 'My terminal', pty }); + * ``` */ - readonly message?: string; + onDidChangeName?: Event; } //#endregion @@ -859,9 +902,37 @@ declare module 'vscode' { export interface TerminalOptions { /** - * A codicon ID to associate with this terminal. + * The icon path or {@link ThemeIcon} for the terminal. */ - readonly icon?: string; + readonly iconPath?: Uri | { light: Uri; dark: Uri } | ThemeIcon; + } + + export interface ExtensionTerminalOptions { + /** + * A themeIcon, Uri, or light and dark Uris to use as the terminal tab icon + */ + readonly iconPath?: Uri | { light: Uri; dark: Uri } | ThemeIcon; + } + + //#endregion + + //#region Terminal profile provider https://github.com/microsoft/vscode/issues/120369 + + export namespace window { + /** + * Registers a provider for a contributed terminal profile. + * @param id The ID of the contributed terminal profile. + * @param provider The terminal profile provider. + */ + export function registerTerminalProfileProvider(id: string, provider: TerminalProfileProvider): Disposable; + } + + export interface TerminalProfileProvider { + /** + * Provide terminal profile options for the requested terminal. + * @param token A cancellation token that indicates the result is no longer needed. + */ + provideProfileOptions(token: CancellationToken): ProviderResult; } //#endregion @@ -924,59 +995,6 @@ declare module 'vscode' { } //#endregion - //#region Status bar item with ID and Name: https://github.com/microsoft/vscode/issues/74972 - - /** - * Options to configure the status bar item. - */ - export interface StatusBarItemOptions { - - /** - * A unique identifier of the status bar item. The identifier - * is for example used to allow a user to show or hide the - * status bar item in the UI. - */ - id: string; - - /** - * A human readable name of the status bar item. The name is - * for example used as a label in the UI to show or hide the - * status bar item. - */ - name: string; - - /** - * Accessibility information used when screen reader interacts with this status bar item. - */ - accessibilityInformation?: AccessibilityInformation; - - /** - * The alignment of the status bar item. - */ - alignment?: StatusBarAlignment; - - /** - * The priority of the status bar item. Higher value means the item should - * be shown more to the left. - */ - priority?: number; - } - - export namespace window { - - /** - * Creates a status bar [item](#StatusBarItem). - * - * @param options The options of the item. If not provided, some default values - * will be assumed. For example, the `StatusBarItemOptions.id` will be the id - * of the extension and the `StatusBarItemOptions.name` will be the extension name. - * @return A new status bar item. - */ - export function createStatusBarItem(options?: StatusBarItemOptions): StatusBarItem; - } - - //#endregion - //#region Custom editor move https://github.com/microsoft/vscode/issues/86146 // TODO: Also for custom editor @@ -987,7 +1005,7 @@ declare module 'vscode' { * Handle when the underlying resource for a custom editor is renamed. * * This allows the webview for the editor be preserved throughout the rename. If this method is not implemented, - * VS Code will destory the previous custom editor and create a replacement one. + * the editor will destroy the previous custom editor and create a replacement one. * * @param newDocument New text document to use for the custom editor. * @param existingWebviewPanel Webview panel for the custom editor. @@ -1012,235 +1030,66 @@ declare module 'vscode' { //#endregion - //#region https://github.com/microsoft/vscode/issues/106744, Notebooks (misc) + //#region https://github.com/microsoft/vscode/issues/124970, Cell Execution State - export enum NotebookCellKind { - // todo@API rename/rethink as "Markup" cell - Markdown = 1, - Code = 2 - } - - export class NotebookCellMetadata { + /** + * The execution state of a notebook cell. + */ + export enum NotebookCellExecutionState { /** - * Whether a code cell's editor is collapsed + * The cell is idle. */ - readonly inputCollapsed?: boolean; - + Idle = 1, /** - * Whether a code cell's outputs are collapsed + * Execution for the cell is pending. */ - readonly outputCollapsed?: boolean; - + Pending = 2, /** - * Additional attributes of a cell metadata. + * The cell is currently executing. */ - readonly [key: string]: any; - - /** - * Create a new notebook cell metadata. - * - * @param inputCollapsed Whether a code cell's editor is collapsed - * @param outputCollapsed Whether a code cell's outputs are collapsed - */ - constructor(inputCollapsed?: boolean, outputCollapsed?: boolean); - - /** - * Derived a new cell metadata from this metadata. - * - * @param change An object that describes a change to this NotebookCellMetadata. - * @return A new NotebookCellMetadata that reflects the given change. Will return `this` NotebookCellMetadata if the change - * is not changing anything. - */ - with(change: { inputCollapsed?: boolean | null, outputCollapsed?: boolean | null, [key: string]: any }): NotebookCellMetadata; - } - - export interface NotebookCellExecutionSummary { - readonly executionOrder?: number; - readonly success?: boolean; - readonly startTime?: number; - readonly endTime?: number; - } - - // 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; - readonly kind: NotebookCellKind; - readonly document: TextDocument; - readonly metadata: NotebookCellMetadata - readonly outputs: ReadonlyArray; - - // todo@API maybe just executionSummary or lastExecutionSummary? - readonly latestExecutionSummary: NotebookCellExecutionSummary | undefined; - } - - export class NotebookDocumentMetadata { - /** - * Whether the document is trusted, default to true - * When false, insecure outputs like HTML, JavaScript, SVG will not be rendered. - */ - readonly trusted: boolean; - - /** - * Additional attributes of the document metadata. - */ - readonly [key: string]: any; - - /** - * Create a new notebook document metadata - * @param trusted Whether the document metadata is trusted. - */ - constructor(trusted?: boolean); - - /** - * Derived a new document metadata from this metadata. - * - * @param change An object that describes a change to this NotebookDocumentMetadata. - * @return A new NotebookDocumentMetadata that reflects the given change. Will return `this` NotebookDocumentMetadata if the change - * is not changing anything. - */ - with(change: { trusted?: boolean | null, [key: string]: any }): NotebookDocumentMetadata - } - - export interface NotebookDocumentContentOptions { - /** - * Controls if outputs change will trigger notebook document content change and if it will be used in the diff editor - * Default to false. If the content provider doesn't persisit the outputs in the file document, this should be set to true. - */ - transientOutputs?: boolean; - - /** - * Controls if a cell metadata property change will trigger notebook document content change and if it will be used in the diff editor - * Default to false. If the content provider doesn't persisit a metadata property in the file document, it should be set to true. - */ - transientCellMetadata?: { [K in keyof NotebookCellMetadata]?: boolean }; - - /** - * Controls if a document metadata property change will trigger notebook document content change and if it will be used in the diff editor - * Default to false. If the content provider doesn't persisit a metadata property in the file document, it should be set to true. - */ - transientDocumentMetadata?: { [K in keyof NotebookDocumentMetadata]?: boolean }; + Executing = 3, } /** - * Represents a notebook. Notebooks are composed of cells and metadata. + * An event describing a cell execution state change. */ - export interface NotebookDocument { + export interface NotebookCellExecutionStateChangeEvent { + /** + * The {@link NotebookCell cell} for which the execution state has changed. + */ + readonly cell: NotebookCell; /** - * The associated uri for this notebook. - * - * *Note* that most notebooks use the `file`-scheme, which means they are files on disk. However, **not** all notebooks are - * saved on disk and therefore the `scheme` must be checked before trying to access the underlying file or siblings on disk. - * - * @see [FileSystemProvider](#FileSystemProvider) - * @see [TextDocumentContentProvider](#TextDocumentContentProvider) + * The new execution state of the cell. */ - readonly uri: Uri; - - // todo@API should we really expose this? - // todo@API should this be called `notebookType` or `notebookKind` - readonly viewType: string; - - /** - * The version number of this notebook (it will strictly increase after each - * change, including undo/redo). - */ - readonly version: number; - - /** - * `true` if there are unpersisted changes. - */ - readonly isDirty: boolean; - - /** - * Is this notebook representing an untitled file which has not been saved yet. - */ - readonly isUntitled: boolean; - - /** - * `true` if the notebook has been closed. A closed notebook isn't synchronized anymore - * and won't be re-used when the same resource is opened again. - */ - readonly isClosed: boolean; - - /** - * The [metadata](#NotebookDocumentMetadata) for this notebook. - */ - readonly metadata: NotebookDocumentMetadata; - - /** - * The number of cells in the notebook. - */ - readonly cellCount: number; - - /** - * Return the cell at the specified index. The index will be adjusted to the notebook. - * - * @param index - The index of the cell to retrieve. - * @return A [cell](#NotebookCell). - */ - cellAt(index: number): NotebookCell; - - /** - * Get the cells of this notebook. A subset can be retrieved by providing - * a range. The range will be adjuset to the notebook. - * - * @param range A notebook range. - * @returns The cells contained by the range or all cells. - */ - getCells(range?: NotebookRange): NotebookCell[]; - - /** - * Save the document. The saving will be handled by the corresponding content provider - * - * @return A promise that will resolve to true when the document - * has been saved. If the file was not dirty or the save failed, - * will return false. - */ - save(): Thenable; + readonly state: NotebookCellExecutionState; } + export namespace notebooks { + + /** + * An {@link Event} which fires when the execution state of a cell has changed. + */ + // todo@API this is an event that is fired for a property that cells don't have and that makes me wonder + // how a correct consumer works, e.g the consumer could have been late and missed an event? + export const onDidChangeNotebookCellExecutionState: Event; + } + + //#endregion + + //#region https://github.com/microsoft/vscode/issues/106744, Notebook, deprecated & misc + + export interface NotebookCellOutput { + id: string; + } + + //#endregion + + //#region https://github.com/microsoft/vscode/issues/106744, NotebookEditor + /** - * A notebook range represents on ordered pair of two cell indicies. - * It is guaranteed that start is less than or equal to end. + * Represents a notebook editor that is attached to a {@link NotebookDocument notebook}. */ - export class NotebookRange { - - /** - * The zero-based start index of this range. - */ - readonly start: number; - - /** - * The exclusive end index of this range (zero-based). - */ - readonly end: number; - - /** - * `true` if `start` and `end` are equal. - */ - readonly isEmpty: boolean; - - /** - * Create a new notebook range. If `start` is not - * before or equal to `end`, the values will be swapped. - * - * @param start start index - * @param end end index. - */ - constructor(start: number, end: number); - - /** - * Derive a new range for this range. - * - * @param change An object that describes a change to this range. - * @return A range that reflects the given change. Will return `this` range if the change - * is not changing anything. - */ - with(change: { start?: number, end?: number }): NotebookRange; - } - export enum NotebookEditorRevealType { /** * The range will be revealed with as little scrolling as possible. @@ -1264,10 +1113,14 @@ declare module 'vscode' { AtTop = 3 } + /** + * Represents a notebook editor that is attached to a {@link NotebookDocument notebook}. + */ export interface NotebookEditor { /** * The document associated with this notebook editor. */ + //todo@api rename to notebook? readonly document: NotebookDocument; /** @@ -1275,7 +1128,7 @@ declare module 'vscode' { * * The primary selection (or focused range) is `selections[0]`. When the document has no cells, the primary selection is empty `{ start: 0, end: 0 }`; */ - readonly selections: NotebookRange[]; + selections: NotebookRange[]; /** * The current visible ranges in the editor (vertically). @@ -1298,8 +1151,9 @@ declare module 'vscode' { export interface NotebookDocumentMetadataChangeEvent { /** - * The [notebook document](#NotebookDocument) for which the document metadata have changed. + * The {@link NotebookDocument notebook document} for which the document metadata have changed. */ + //todo@API rename to notebook? readonly document: NotebookDocument; } @@ -1315,31 +1169,36 @@ declare module 'vscode' { export interface NotebookCellsChangeEvent { /** - * The [notebook document](#NotebookDocument) for which the cells have changed. + * The {@link NotebookDocument notebook document} for which the cells have changed. */ + //todo@API rename to notebook? readonly document: NotebookDocument; readonly changes: ReadonlyArray; } export interface NotebookCellOutputsChangeEvent { /** - * The [notebook document](#NotebookDocument) for which the cell outputs have changed. + * The {@link NotebookDocument notebook document} for which the cell outputs have changed. */ + //todo@API remove? use cell.notebook instead? readonly document: NotebookDocument; + // NotebookCellOutputsChangeEvent.cells vs NotebookCellMetadataChangeEvent.cell readonly cells: NotebookCell[]; } export interface NotebookCellMetadataChangeEvent { /** - * The [notebook document](#NotebookDocument) for which the cell metadata have changed. + * The {@link NotebookDocument notebook document} for which the cell metadata have changed. */ + //todo@API remove? use cell.notebook instead? readonly document: NotebookDocument; + // NotebookCellOutputsChangeEvent.cells vs NotebookCellMetadataChangeEvent.cell readonly cell: NotebookCell; } export interface NotebookEditorSelectionChangeEvent { /** - * The [notebook editor](#NotebookEditor) for which the selections have changed. + * The {@link NotebookEditor notebook editor} for which the selections have changed. */ readonly notebookEditor: NotebookEditor; readonly selections: ReadonlyArray @@ -1347,40 +1206,12 @@ declare module 'vscode' { export interface NotebookEditorVisibleRangesChangeEvent { /** - * The [notebook editor](#NotebookEditor) for which the visible ranges have changed. + * The {@link NotebookEditor notebook editor} for which the visible ranges have changed. */ readonly notebookEditor: NotebookEditor; readonly visibleRanges: ReadonlyArray; } - export interface NotebookCellExecutionStateChangeEvent { - /** - * The [notebook document](#NotebookDocument) for which the cell execution state has changed. - */ - readonly document: NotebookDocument; - readonly cell: NotebookCell; - readonly executionState: NotebookCellExecutionState; - } - - // todo@API support ids https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md - export class NotebookCellData { - kind: NotebookCellKind; - // todo@API better names: value? text? - source: string; - // todo@API languageId (as in TextDocument) - language: string; - outputs?: NotebookCellOutput[]; - metadata?: NotebookCellMetadata; - // todo@API just executionSummary or lastExecutionSummary - latestExecutionSummary?: NotebookCellExecutionSummary; - constructor(kind: NotebookCellKind, source: string, language: string, outputs?: NotebookCellOutput[], metadata?: NotebookCellMetadata, latestExecutionSummary?: NotebookCellExecutionSummary); - } - - export class NotebookData { - cells: NotebookCellData[]; - metadata: NotebookDocumentMetadata; - constructor(cells: NotebookCellData[], metadata?: NotebookDocumentMetadata); - } export interface NotebookDocumentShowOptions { viewColumn?: ViewColumn; @@ -1389,19 +1220,12 @@ declare module 'vscode' { selections?: NotebookRange[]; } - export namespace notebook { + export namespace notebooks { - export function openNotebookDocument(uri: Uri): Thenable; - 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; @@ -1426,38 +1250,6 @@ declare module 'vscode' { //#endregion - //#region https://github.com/microsoft/vscode/issues/106744, NotebookCellOutput - - // code specific mime types - // application/x.notebook.error-traceback - // application/x.notebook.stdout - // application/x.notebook.stderr - // application/x.notebook.stream - export class NotebookCellOutputItem { - - // todo@API - // add factory functions for common mime types - // static textplain(value:string): NotebookCellOutputItem; - // static errortrace(value:any): NotebookCellOutputItem; - - mime: string; - value: unknown; - metadata?: Record; - - constructor(mime: string, value: unknown, metadata?: Record); - } - - // @jrieken transient - export class NotebookCellOutput { - id: string; - outputs: NotebookCellOutputItem[]; - metadata?: Record; - constructor(outputs: NotebookCellOutputItem[], metadata?: Record); - constructor(outputs: NotebookCellOutputItem[], id: string, metadata?: Record); - } - - //#endregion - //#region https://github.com/microsoft/vscode/issues/106744, NotebookEditorEdit // todo@API add NotebookEdit-type which handles all these cases? @@ -1478,26 +1270,26 @@ declare module 'vscode' { export interface WorkspaceEdit { // todo@API add NotebookEdit-type which handles all these cases? - replaceNotebookMetadata(uri: Uri, value: NotebookDocumentMetadata): void; + replaceNotebookMetadata(uri: Uri, value: { [key: string]: any }): void; replaceNotebookCells(uri: Uri, range: NotebookRange, cells: NotebookCellData[], metadata?: WorkspaceEditEntryMetadata): void; - replaceNotebookCellMetadata(uri: Uri, index: number, cellMetadata: NotebookCellMetadata, metadata?: WorkspaceEditEntryMetadata): void; + replaceNotebookCellMetadata(uri: Uri, index: number, cellMetadata: { [key: string]: any }, metadata?: WorkspaceEditEntryMetadata): void; } export interface NotebookEditorEdit { - replaceMetadata(value: NotebookDocumentMetadata): void; + replaceMetadata(value: { [key: string]: any }): void; replaceCells(start: number, end: number, cells: NotebookCellData[]): void; - replaceCellMetadata(index: number, metadata: NotebookCellMetadata): void; + replaceCellMetadata(index: number, metadata: { [key: string]: any }): 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 + * The given callback-function is invoked with an {@link NotebookEditorEdit edit-builder} 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). + * @param callback A function which can create edits using an {@link NotebookEditorEdit edit-builder}. * @return A promise that resolves with a value indicating if the edits could be applied. */ // @jrieken REMOVE maybe @@ -1506,368 +1298,6 @@ declare module 'vscode' { //#endregion - //#region https://github.com/microsoft/vscode/issues/106744, NotebookSerializer - - /** - * The notebook serializer enables the editor to open notebook files. - * - * At its core the editor only knows a [notebook data structure](#NotebookData) but not - * how that data structure is written to a file, nor how it is read from a file. The - * notebook serializer bridges this gap by deserializing bytes into notebook data and - * vice versa. - */ - export interface NotebookSerializer { - - /** - * Deserialize contents of a notebook file into the notebook data structure. - * - * @param content Contents of a notebook file. - * @param token A cancellation token. - * @return Notebook data or a thenable that resolves to such. - */ - deserializeNotebook(content: Uint8Array, token: CancellationToken): NotebookData | Thenable; - - /** - * Serialize notebook data into file contents. - * - * @param data A notebook data structure. - * @param token A cancellation token. - * @returns An array of bytes or a thenable that resolves to such. - */ - serializeNotebook(data: NotebookData, token: CancellationToken): Uint8Array | Thenable; - } - - export namespace notebook { - - // todo@API remove output when notebook marks that as transient, same for metadata - /** - * Register a [notebook serializer](#NotebookSerializer). - * - * @param notebookType A notebook. - * @param serializer A notebook serialzier. - * @param options Optional context options that define what parts of a notebook should be persisted - * @return A [disposable](#Disposable) that unregisters this serializer when being disposed. - */ - export function registerNotebookSerializer(notebookType: string, serializer: NotebookSerializer, options?: NotebookDocumentContentOptions): Disposable; - } - - //#endregion - - //#region https://github.com/microsoft/vscode/issues/119949 - - - export interface NotebookFilter { - readonly viewType?: string; - readonly scheme?: string; - readonly pattern?: GlobPattern; - } - - export type NotebookSelector = NotebookFilter | string | ReadonlyArray; - - export interface NotebookExecuteHandler { - /** - * @param cells The notebook cells to execute. - * @param notebook The notebook for which the execute handler is being called. - * @param controller The controller that the handler is attached to - */ - (this: NotebookController, cells: NotebookCell[], notebook: NotebookDocument, controller: NotebookController): void | Thenable - } - - export interface NotebookInterruptHandler { - /** - * @param notebook The notebook for which the interrupt handler is being called. - */ - (this: NotebookController, notebook: NotebookDocument): void | Thenable; - } - - export interface NotebookController { - - /** - * The identifier of this notebook controller. - */ - readonly id: string; - - /** - * The notebook view type this controller is for. - */ - readonly viewType: string; - - /** - * An array of language identifiers that are supported by this - * controller. Any language identifier from [`getLanguages`](#languages.getLanguages) - * is possible. When falsy all languages are supported. - * - * Samples: - * ```js - * // support JavaScript and TypeScript - * myController.supportedLanguages = ['javascript', 'typescript'] - * - * // support all languages - * myController.supportedLanguages = undefined; // falsy - * myController.supportedLanguages = []; // falsy - * ``` - */ - supportedLanguages?: string[]; - - /** - * The human-readable label of this notebook controller. - */ - label: string; - - /** - * The human-readable description which is rendered less prominent. - */ - description?: string; - - /** - * The human-readable detail which is rendered less prominent. - */ - detail?: string; - - /** - * Whether this controller supports execution order so that the - * editor can render placeholders for them. - */ - // todo@API rename to supportsExecutionOrder, usesExecutionOrder - hasExecutionOrder?: boolean; - - /** - * The execute handler is invoked when the run gestures in the UI are selected, e.g Run Cell, Run All, - * Run Selection etc. - */ - executeHandler: NotebookExecuteHandler; - - /** - * The interrupt handler is invoked the interrupt all execution. This is contrary to cancellation (available via - * [`NotebookCellExecutionTask#token`](NotebookCellExecutionTask#token)) and should only be used when - * execution-level cancellation is supported - */ - interruptHandler?: NotebookInterruptHandler - - /** - * Dispose and free associated resources. - */ - dispose(): void; - - /** - * A kernel can apply to one or many notebook documents but a notebook has only one active - * kernel. This event fires whenever a notebook has been associated to a kernel or when - * that association has been removed. - */ - readonly onDidChangeNotebookAssociation: Event<{ notebook: NotebookDocument, selected: boolean }>; - - /** - * Create a cell execution task. - * - * This should be used in response to the [execution handler](#NotebookController.executeHandler) - * being calleed or when cell execution has been started else, e.g when a cell was already - * executing or when cell execution was triggered from another source. - * - * @param cell The notebook cell for which to create the execution. - * @returns A notebook cell execution. - */ - createNotebookCellExecutionTask(cell: NotebookCell): NotebookCellExecutionTask; - - // todo@API find a better name than "preloads" - // todo@API allow add, not remove - // ipc - readonly preloads: NotebookKernelPreload[]; - - /** - * An event that fires when a renderer (see `preloads`) has send a message to the controller. - */ - readonly onDidReceiveMessage: Event<{ editor: NotebookEditor, message: any }>; - - /** - * Send a message to the renderer of notebook editors. - * - * Note that only editors showing documents that are bound to this controller - * are receiving the message. - * - * @param message The message to send. - * @param editor A specific editor to send the message to. When `undefined` all applicable editors are receiving the message. - * @returns A promise that resolves to a boolean indicating if the message has been send or not. - */ - postMessage(message: any, editor?: NotebookEditor): Thenable; - - //todo@API validate this works - asWebviewUri(localResource: Uri): Uri; - - /** - * A controller can set affinities for specific notebook documents. This allows a controller - * to be more important for some notebooks. - * - * @param notebook The notebook for which a priority is set. - * @param affinity A controller affinity - */ - updateNotebookAffinity(notebook: NotebookDocument, affinity: NotebookControllerAffinity): void; - } - - export enum NotebookControllerAffinity { - Default = 1, - Preferred = 2 - } - - export namespace notebook { - - /** - * Creates a new notebook controller. - * - * @param id Extension-unique identifier of the controller - * @param viewType A notebook type for which this controller is for. - * @param label The label of the controller - * @param handler - * @param preloads - */ - export function createNotebookController(id: string, viewType: string, label: string, handler?: NotebookExecuteHandler, preloads?: NotebookKernelPreload[]): NotebookController; - } - - //#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; - readonly untitledDocumentData?: Uint8Array; - } - - // todo@API use openNotebookDOCUMENT to align with openCustomDocument etc? - // todo@API rename to NotebookDocumentContentProvider - export interface NotebookContentProvider { - - readonly options?: NotebookDocumentContentOptions; - readonly onDidChangeNotebookContentOptions?: 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, token: CancellationToken): NotebookData | Thenable; - - // todo@API use NotebookData instead - saveNotebook(document: NotebookDocument, token: CancellationToken): Thenable; - - // todo@API use NotebookData instead - saveNotebookAs(targetResource: Uri, document: NotebookDocument, token: CancellationToken): Thenable; - - // todo@API use NotebookData instead - backupNotebook(document: NotebookDocument, context: NotebookDocumentBackupContext, token: CancellationToken): Thenable; - } - - export interface NotebookDocumentContentOptions { - /** - * Not ready for production or development use yet. - */ - viewOptions?: { - displayName: string; - filenamePattern: (GlobPattern | { include: GlobPattern; exclude: GlobPattern; })[]; - exclusive?: boolean; - }; - } - - 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): Disposable; - } - - //#endregion - - //#region https://github.com/microsoft/vscode/issues/106744, NotebookKernel - - // todo@API make class? - export interface NotebookKernelPreload { - provides?: string | string[]; - uri: Uri; - } - - export interface NotebookCellExecuteStartContext { - /** - * The time that execution began, in milliseconds in the Unix epoch. Used to drive the clock - * that shows for how long a cell has been running. If not given, the clock won't be shown. - */ - startTime?: number; - } - - export interface NotebookCellExecuteEndContext { - /** - * If true, a green check is shown on the cell status bar. - * If false, a red X is shown. - */ - success?: boolean; - - /** - * The time that execution finished, in milliseconds in the Unix epoch. - */ - endTime?: number; - } - - /** - * A NotebookCellExecutionTask is how the kernel modifies a notebook cell as it is executing. When - * [`createNotebookCellExecutionTask`](#notebook.createNotebookCellExecutionTask) is called, the cell - * enters the Pending state. When `start()` is called on the execution task, it enters the Executing state. When - * `end()` is called, it enters the Idle state. While in the Executing state, cell outputs can be - * modified with the methods on the run task. - * - * All outputs methods operate on this NotebookCellExecutionTask's cell by default. They optionally take - * a cellIndex parameter that allows them to modify the outputs of other cells. `appendOutputItems` and - * `replaceOutputItems` operate on the output with the given ID, which can be an output on any cell. They - * all resolve once the output edit has been applied. - */ - export interface NotebookCellExecutionTask { - readonly document: NotebookDocument; - readonly cell: NotebookCell; - - start(context?: NotebookCellExecuteStartContext): void; - executionOrder: number | undefined; - end(result?: NotebookCellExecuteEndContext): void; - readonly token: CancellationToken; - - clearOutput(cellIndex?: number): Thenable; - appendOutput(out: NotebookCellOutput | NotebookCellOutput[], cellIndex?: number): Thenable; - replaceOutput(out: NotebookCellOutput | NotebookCellOutput[], cellIndex?: number): Thenable; - appendOutputItems(items: NotebookCellOutputItem | NotebookCellOutputItem[], outputId: string): Thenable; - replaceOutputItems(items: NotebookCellOutputItem | NotebookCellOutputItem[], outputId: string): Thenable; - } - - export enum NotebookCellExecutionState { - Idle = 1, - Pending = 2, - Executing = 3, - } - - export namespace notebook { - /** @deprecated use NotebookController */ - export function createNotebookCellExecutionTask(uri: Uri, index: number, kernelId: string): NotebookCellExecutionTask | undefined; - - export const onDidChangeCellExecutionState: Event; - } - - //#endregion - //#region https://github.com/microsoft/vscode/issues/106744, NotebookEditorDecorationType export interface NotebookEditor { @@ -1885,62 +1315,15 @@ declare module 'vscode' { dispose(): void; } - export namespace notebook { + export namespace notebooks { export function createNotebookEditorDecorationType(options: NotebookDecorationRenderOptions): NotebookEditorDecorationType; } //#endregion - //#region https://github.com/microsoft/vscode/issues/106744, NotebookCellStatusBarItem - - /** - * Represents the alignment of status bar items. - */ - export enum NotebookCellStatusBarAlignment { - - /** - * Aligned to the left side. - */ - Left = 1, - - /** - * Aligned to the right side. - */ - Right = 2 - } - - export class NotebookCellStatusBarItem { - text: string; - alignment: NotebookCellStatusBarAlignment; - command?: string | Command; - tooltip?: string; - priority?: number; - accessibilityInformation?: AccessibilityInformation; - - constructor(text: string, alignment: NotebookCellStatusBarAlignment, command?: string | Command, tooltip?: string, priority?: number, accessibilityInformation?: AccessibilityInformation); - } - - interface NotebookCellStatusBarItemProvider { - /** - * Implement and fire this event to signal that statusbar items have changed. The provide method will be called again. - */ - onDidChangeCellStatusBarItems?: Event; - - /** - * The provider will be called when the cell scrolls into view, when its content, outputs, language, or metadata change, and when it changes execution state. - */ - provideCellStatusBarItems(cell: NotebookCell, token: CancellationToken): ProviderResult; - } - - export namespace notebook { - export function registerNotebookCellStatusBarItemProvider(selector: NotebookSelector, provider: NotebookCellStatusBarItemProvider): Disposable; - } - - //#endregion - //#region https://github.com/microsoft/vscode/issues/106744, NotebookConcatTextDocument - export namespace notebook { + export namespace notebooks { /** * 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. @@ -1973,6 +1356,84 @@ declare module 'vscode' { //#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 the editor 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; + readonly untitledDocumentData?: Uint8Array; + } + + // todo@API use openNotebookDOCUMENT to align with openCustomDocument etc? + // todo@API rename to NotebookDocumentContentProvider + export interface NotebookContentProvider { + + readonly options?: NotebookDocumentContentOptions; + readonly onDidChangeNotebookContentOptions?: Event; + + /** + * Content providers should always use {@link FileSystemProvider file system providers} to + * resolve the raw content for `uri` as the resouce is not necessarily a file on disk. + */ + openNotebook(uri: Uri, openContext: NotebookDocumentOpenContext, token: CancellationToken): NotebookData | Thenable; + + // todo@API use NotebookData instead + saveNotebook(document: NotebookDocument, token: CancellationToken): Thenable; + + // todo@API use NotebookData instead + saveNotebookAs(targetResource: Uri, document: NotebookDocument, token: CancellationToken): Thenable; + + // todo@API use NotebookData instead + backupNotebook(document: NotebookDocument, context: NotebookDocumentBackupContext, token: CancellationToken): Thenable; + } + + export namespace workspace { + + // 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): Disposable; + } + + //#endregion + + //#region https://github.com/microsoft/vscode/issues/106744, LiveShare + + export interface NotebookRegistrationData { + displayName: string; + filenamePattern: (GlobPattern | { include: GlobPattern; exclude: GlobPattern; })[]; + exclusive?: boolean; + } + + export namespace workspace { + // SPECIAL overload with NotebookRegistrationData + export function registerNotebookContentProvider(notebookType: string, provider: NotebookContentProvider, options?: NotebookDocumentContentOptions, registrationData?: NotebookRegistrationData): Disposable; + // SPECIAL overload with NotebookRegistrationData + export function registerNotebookSerializer(notebookType: string, serializer: NotebookSerializer, options?: NotebookDocumentContentOptions, registration?: NotebookRegistrationData): Disposable; + } + + //#endregion + //#region https://github.com/microsoft/vscode/issues/39441 export interface CompletionItem { @@ -1991,11 +1452,13 @@ declare module 'vscode' { /** * The parameters without the return type. Render after `name`. */ + //todo@API rename to signature parameters?: string; /** * The fully qualified name, like package name or file path. Rendered after `signature`. */ + //todo@API find better name qualifier?: string; /** @@ -2006,6 +1469,109 @@ declare module 'vscode' { //#endregion + //#region @https://github.com/microsoft/vscode/issues/123601, notebook messaging + + export interface NotebookRendererMessage { + /** + * Editor that sent the message. + */ + editor: NotebookEditor; + + /** + * Message sent from the webview. + */ + message: T; + } + + /** + * Renderer messaging is used to communicate with a single renderer. It's + * returned from {@link notebooks.createRendererMessaging}. + */ + export interface NotebookRendererMessaging { + /** + * Events that fires when a message is received from a renderer. + */ + onDidReceiveMessage: Event>; + + /** + * Sends a message to the renderer. + * @param editor Editor to target with the message + * @param message Message to send + */ + postMessage(editor: NotebookEditor, message: TSend): void; + } + + /** + * Represents a script that is loaded into the notebook renderer before rendering output. This allows + * to provide and share functionality for notebook markup and notebook output renderers. + */ + export class NotebookRendererScript { + + /** + * APIs that the preload provides to the renderer. These are matched + * against the `dependencies` and `optionalDependencies` arrays in the + * notebook renderer contribution point. + */ + provides: string[]; + + /** + * URI for the file to preload + */ + uri: Uri; + + /** + * @param uri URI for the file to preload + * @param provides Value for the `provides` property + */ + constructor(uri: Uri, provides?: string | string[]); + } + + export interface NotebookController { + + // todo@API allow add, not remove + readonly rendererScripts: NotebookRendererScript[]; + + /** + * An event that fires when a {@link NotebookController.rendererScripts renderer script} has send a message to + * the controller. + */ + readonly onDidReceiveMessage: Event<{ editor: NotebookEditor, message: any }>; + + /** + * Send a message to the renderer of notebook editors. + * + * Note that only editors showing documents that are bound to this controller + * are receiving the message. + * + * @param message The message to send. + * @param editor A specific editor to send the message to. When `undefined` all applicable editors are receiving the message. + * @returns A promise that resolves to a boolean indicating if the message has been send or not. + */ + postMessage(message: any, editor?: NotebookEditor): Thenable; + + //todo@API validate this works + asWebviewUri(localResource: Uri): Uri; + } + + export namespace notebooks { + + export function createNotebookController(id: string, viewType: string, label: string, handler?: (cells: NotebookCell[], notebook: NotebookDocument, controller: NotebookController) => void | Thenable, rendererScripts?: NotebookRendererScript[]): NotebookController; + + /** + * Creates a new messaging instance used to communicate with a specific + * renderer. The renderer only has access to messaging if `requiresMessaging` + * is set to `always` or `optional` in its `notebookRenderer ` contribution. + * + * @see https://github.com/microsoft/vscode/issues/123601 + * @param rendererId The renderer ID to communicate with + */ + // todo@API can ANY extension talk to renderer or is there a check that the calling extension + // declared the renderer in its package.json? + export function createRendererMessaging(rendererId: string): NotebookRendererMessaging; + } + + //#endregion + //#region @eamodio - timeline: https://github.com/microsoft/vscode/issues/84297 export class TimelineItem { @@ -2027,7 +1593,7 @@ declare module 'vscode' { id?: string; /** - * The icon path or [ThemeIcon](#ThemeIcon) for the timeline item. + * The icon path or {@link ThemeIcon} for the timeline item. */ iconPath?: Uri | { light: Uri; dark: Uri; } | ThemeIcon; @@ -2042,7 +1608,7 @@ declare module 'vscode' { detail?: string; /** - * The [command](#Command) that should be executed when the timeline item is selected. + * The {@link Command} that should be executed when the timeline item is selected. */ command?: Command; @@ -2080,7 +1646,7 @@ declare module 'vscode' { export interface TimelineChangeEvent { /** - * The [uri](#Uri) of the resource for which the timeline changed. + * The {@link Uri} of the resource for which the timeline changed. */ uri: Uri; @@ -2100,7 +1666,7 @@ declare module 'vscode' { }; /** - * An array of [timeline items](#TimelineItem). + * An array of {@link TimelineItem timeline items}. */ readonly items: readonly TimelineItem[]; } @@ -2136,12 +1702,12 @@ declare module 'vscode' { readonly label: string; /** - * Provide [timeline items](#TimelineItem) for a [Uri](#Uri). + * Provide {@link TimelineItem timeline items} for a {@link Uri}. * - * @param uri The [uri](#Uri) of the file to provide the timeline for. + * @param uri The {@link Uri} of the file to provide the timeline for. * @param options A set of options to determine how results should be returned. * @param token A cancellation token. - * @return The [timeline result](#TimelineResult) or a thenable that resolves to such. The lack of a result + * @return The {@link TimelineResult timeline result} or a thenable that resolves to such. The lack of a result * can be signaled by returning `undefined`, `null`, or an empty array. */ provideTimeline(uri: Uri, options: TimelineOptions, token: CancellationToken): ProviderResult; @@ -2157,7 +1723,7 @@ declare module 'vscode' { * * @param scheme A scheme or schemes that defines which documents this provider is applicable to. Can be `*` to target all documents. * @param provider A timeline provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerTimelineProvider(scheme: string | string[], provider: TimelineProvider): Disposable; } @@ -2186,49 +1752,49 @@ declare module 'vscode' { //#region https://github.com/microsoft/vscode/issues/16221 - // todo@API rename to InlayHint + // todo@API Split between Inlay- and OverlayHints (InlayHint are for a position, OverlayHints for a non-empty range) // todo@API add "mini-markdown" for links and styles - // todo@API remove description - // (done:) add InlayHintKind with type, argument, etc + // (done) remove description + // (done) rename to InlayHint + // (done) add InlayHintKind with type, argument, etc export namespace languages { /** - * Register a inline hints provider. + * Register a inlay 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. + * @param provider An inlay hints provider. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ - export function registerInlineHintsProvider(selector: DocumentSelector, provider: InlineHintsProvider): Disposable; + export function registerInlayHintsProvider(selector: DocumentSelector, provider: InlayHintsProvider): Disposable; } - export enum InlineHintKind { + export enum InlayHintKind { Other = 0, Type = 1, Parameter = 2, } /** - * Inline hint information. + * Inlay hint information. */ - export class InlineHint { + export class InlayHint { /** * The text of the hint. */ text: string; /** - * The range of the hint. + * The position of this hint. */ - range: Range; - - kind?: InlineHintKind; - - // todo@API remove this - description?: string | MarkdownString; + position: Position; + /** + * The kind of this hint. + */ + kind?: InlayHintKind; /** * Whitespace before the hint. */ @@ -2239,29 +1805,29 @@ declare module 'vscode' { whitespaceAfter?: boolean; // todo@API make range first argument - constructor(text: string, range: Range, kind?: InlineHintKind); + constructor(text: string, position: Position, kind?: InlayHintKind); } /** - * The inline hints provider interface defines the contract between extensions and - * the inline hints feature. + * The inlay hints provider interface defines the contract between extensions and + * the inlay hints feature. */ - export interface InlineHintsProvider { + export interface InlayHintsProvider { /** - * An optional event to signal that inline hints have changed. - * @see [EventEmitter](#EventEmitter) + * An optional event to signal that inlay hints have changed. + * @see {@link EventEmitter} */ - onDidChangeInlineHints?: Event; + onDidChangeInlayHints?: 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. + * @param model The document in which the command was invoked. + * @param range The range for which inlay hints should be computed. + * @param token A cancellation token. + * @return A list of inlay hints or a thenable that resolves to such. */ - provideInlineHints(model: TextDocument, range: Range, token: CancellationToken): ProviderResult; + provideInlayHints(model: TextDocument, range: Range, token: CancellationToken): ProviderResult; } //#endregion @@ -2289,7 +1855,7 @@ declare module 'vscode' { export interface TextDocument { /** - * The [notebook](#NotebookDocument) that contains this document as a notebook cell or `undefined` when + * The {@link NotebookDocument notebook} that contains this document as a notebook cell or `undefined` when * the document is not contained by a notebook (this should be the more frequent case). */ notebook: NotebookDocument | undefined; @@ -2336,7 +1902,7 @@ declare module 'vscode' { * disambiguate multiple sets of results in a test run. It is useful if * tests are run across multiple platforms, for example. * @param persist Whether the results created by the run should be - * persisted in VS Code. This may be false if the results are coming from + * persisted in the editor. This may be false if the results are coming from * a file already saved externally, such as a coverage information file. */ export function createTestRun(request: TestRunRequest, name?: string, persist?: boolean): TestRun; @@ -2355,7 +1921,7 @@ declare module 'vscode' { export function createTestItem(options: TestItemOptions): TestItem; /** - * List of test results stored by VS Code, sorted in descnding + * List of test results stored by the editor, sorted in descending * order by their `completedAt` time. * @stability experimental */ @@ -2395,7 +1961,7 @@ declare module 'vscode' { readonly onDidDiscoverInitialTests: Event; /** - * Dispose of the observer, allowing VS Code to eventually tell test + * Dispose of the observer, allowing the editor to eventually tell test * providers that they no longer need to update tests. */ dispose(): void; @@ -2482,7 +2048,7 @@ declare module 'vscode' { tests: TestItem[]; /** - * An array of tests the user has marked as excluded in VS Code. May be + * An array of tests the user has marked as excluded in the editor. May be * omitted if no exclusions were requested. Test controllers should not run * excluded tests or any children of excluded tests. */ @@ -2574,7 +2140,7 @@ declare module 'vscode' { /** * URI this TestItem is associated with. May be a file or directory. */ - uri: Uri; + uri?: Uri; /** * Display name describing the test item. @@ -2597,7 +2163,7 @@ declare module 'vscode' { /** * URI this TestItem is associated with. May be a file or directory. */ - readonly uri: Uri; + readonly uri?: Uri; /** * A mapping of children by ID to the associated TestItem instances. @@ -2786,7 +2352,7 @@ declare module 'vscode' { } /** - * TestResults can be provided to VS Code in {@link test.publishTestResult}, + * TestResults can be provided to the editor in {@link test.publishTestResult}, * or read from it in {@link test.testResults}. * * The results contain a 'snapshot' of the tests at the point when the test @@ -2829,7 +2395,7 @@ declare module 'vscode' { /** * URI this TestItem is associated with. May be a file or file. */ - readonly uri: Uri; + readonly uri?: Uri; /** * Display name describing the test case. @@ -2889,7 +2455,7 @@ declare module 'vscode' { * 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`. + * The editor 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. */ @@ -2904,21 +2470,21 @@ declare module 'vscode' { /** * 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. + * since the editor always contributes a built-in `Default` opener. */ Option = 1, /** * The opener can open the uri. * - * VS Code's built-in opener has `Default` priority. This means that any additional `Default` + * The editor'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. + * default openers, include the built-in one from the editor. * * A preferred opener will be automatically selected if no other preferred openers * are available. If multiple preferred openers are available, then the user @@ -2931,7 +2497,7 @@ declare module 'vscode' { * 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. + * inside of the editor instead of having the link be opened by the web browser. * * Currently openers may only be registered for `http` and `https` uris. */ @@ -3022,11 +2588,11 @@ declare module 'vscode' { * 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 + * If `true`, the editor 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 + * that should be used if it is available. Use `'default'` to force the editor's * standard external opener to be used. */ readonly allowContributedOpeners?: boolean | string; @@ -3038,12 +2604,76 @@ declare module 'vscode' { //#endregion + //#region @joaomoreno https://github.com/microsoft/vscode/issues/124263 + // This API change only affects behavior and documentation, not API surface. + + namespace env { + + /** + * Resolves a uri to form that is accessible externally. + * + * #### `http:` or `https:` scheme + * + * Resolves an *external* uri, such as a `http:` or `https:` link, from where the extension is running to a + * uri to the same resource on the client machine. + * + * This is a no-op if the extension is running on the client machine. + * + * If the extension is running remotely, this function automatically establishes a port forwarding tunnel + * from the local machine to `target` on the remote and returns a local uri to the tunnel. The lifetime of + * the port forwarding tunnel is managed by the editor and the tunnel can be closed by the user. + * + * *Note* that uris passed through `openExternal` are automatically resolved and you should not call `asExternalUri` on them. + * + * #### `vscode.env.uriScheme` + * + * Creates a uri that - if opened in a browser (e.g. via `openExternal`) - will result in a registered {@link UriHandler} + * to trigger. + * + * Extensions should not make any assumptions about the resulting uri and should not alter it in anyway. + * Rather, extensions can e.g. use this uri in an authentication flow, by adding the uri as callback query + * argument to the server to authenticate to. + * + * *Note* that if the server decides to add additional query parameters to the uri (e.g. a token or secret), it + * will appear in the uri that is passed to the {@link UriHandler}. + * + * **Example** of an authentication flow: + * ```typescript + * vscode.window.registerUriHandler({ + * handleUri(uri: vscode.Uri): vscode.ProviderResult { + * if (uri.path === '/did-authenticate') { + * console.log(uri.toString()); + * } + * } + * }); + * + * const callableUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://my.extension/did-authenticate`)); + * await vscode.env.openExternal(callableUri); + * ``` + * + * *Note* that extensions should not cache the result of `asExternalUri` as the resolved uri may become invalid due to + * a system or user action — for example, in remote cases, a user may close a port forwarding tunnel that was opened by + * `asExternalUri`. + * + * #### Any other scheme + * + * Any other scheme will be handled as if the provided URI is a workspace URI. In that case, the method will return + * a URI which, when handled, will make the editor open the workspace. + * + * @return A uri that can be used on the client machine. + */ + export function asExternalUri(target: Uri): Thenable; + } + + //#endregion + //#region https://github.com/Microsoft/vscode/issues/15178 // TODO@API must be a class export interface OpenEditorInfo { name: string; resource: Uri; + isActive: boolean; } export namespace window { @@ -3061,47 +2691,24 @@ declare module 'vscode' { */ export interface WorkspaceTrustRequestOptions { /** - * When true, a modal dialog will be used to request workspace trust. - * When false, a badge will be displayed on the settings gear activity bar item. + * Custom message describing the user action that requires workspace + * trust. If omitted, a generic message will be displayed in the workspace + * trust request dialog. */ - readonly modal: boolean; + readonly message?: string; } export namespace workspace { /** * Prompt the user to chose whether to trust the current workspace * @param options Optional object describing the properties of the - * workspace trust request. Defaults to { modal: false } - * When using a non-modal request, the promise will return immediately. - * Any time trust is not given, it is recommended to use the - * `onDidGrantWorkspaceTrust` event to listen for trust changes. + * workspace trust request. */ export function requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Thenable; } //#endregion - //#region https://github.com/microsoft/vscode/issues/115807 - - export interface Webview { - /** - * @param message A json serializable message to send to the webview. - * - * For older versions of vscode, if an `ArrayBuffer` is included in `message`, - * it will not be serialized properly and will not be received by the webview. - * Similarly any TypedArrays, such as a `Uint8Array`, will be very inefficiently - * serialized and will also not be recreated as a typed array inside the webview. - * - * However if your extension targets vscode 1.56+ in the `engines` field of its - * `package.json` any `ArrayBuffer` values that appear in `message` will be more - * efficiently transferred to the webview and will also be recreated inside of - * the webview. - */ - postMessage(message: any): Thenable; - } - - //#endregion - //#region https://github.com/microsoft/vscode/issues/115616 @alexr00 export enum PortAutoForwardAction { Notify = 1, @@ -3111,9 +2718,23 @@ declare module 'vscode' { Ignore = 5 } - export interface PortAttributes { + export class PortAttributes { + /** + * The port number associated with this this set of attributes. + */ port: number; - autoForwardAction: PortAutoForwardAction + + /** + * The action to be taken when this port is detected for auto forwarding. + */ + autoForwardAction: PortAutoForwardAction; + + /** + * Creates a new PortAttributes object + * @param port the port number + * @param autoForwardAction the action to take when this port is detected + */ + constructor(port: number, autoForwardAction: PortAutoForwardAction); } export interface PortAttributesProvider { @@ -3129,7 +2750,7 @@ declare module 'vscode' { /** * If your extension listens on ports, consider registering a PortAttributesProvider to provide information * about the ports. For example, a debug extension may know about debug ports in it's debuggee. By providing - * this information with a PortAttributesProvider the extension can tell VS Code that these ports should be + * this information with a PortAttributesProvider the extension can tell the editor that these ports should be * ignored, since they don't need to be user facing. * * @param portSelector If registerPortAttributesProvider is called after you start your process then you may already @@ -3142,7 +2763,7 @@ declare module 'vscode' { } //#endregion - // region https://github.com/microsoft/vscode/issues/119904 @eamodio + //#region https://github.com/microsoft/vscode/issues/119904 @eamodio export interface SourceControlInputBox { @@ -3153,4 +2774,148 @@ declare module 'vscode' { } //#endregion + + //#region https://github.com/microsoft/vscode/issues/124024 @hediet @alexdima + + export namespace languages { + /** + * Registers an inline completion provider. + */ + export function registerInlineCompletionItemProvider(selector: DocumentSelector, provider: InlineCompletionItemProvider): Disposable; + } + + export interface InlineCompletionItemProvider { + /** + * Provides inline completion items for the given position and document. + * If inline completions are enabled, this method will be called whenever the user stopped typing. + * It will also be called when the user explicitly triggers inline completions or asks for the next or previous inline completion. + * Use `context.triggerKind` to distinguish between these scenarios. + */ + provideInlineCompletionItems(document: TextDocument, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult | T[]>; + } + + export interface InlineCompletionContext { + /** + * How the completion was triggered. + */ + readonly triggerKind: InlineCompletionTriggerKind; + } + + /** + * How an {@link InlineCompletionItemProvider inline completion provider} was triggered. + */ + export enum InlineCompletionTriggerKind { + /** + * Completion was triggered automatically while editing. + * It is sufficient to return a single completion item in this case. + */ + Automatic = 0, + + /** + * Completion was triggered explicitly by a user gesture. + * Return multiple completion items to enable cycling through them. + */ + Explicit = 1, + } + + export class InlineCompletionList { + items: T[]; + + constructor(items: T[]); + } + + export class InlineCompletionItem { + /** + * The text to insert. + * If the text contains a line break, the range must end at the end of a line. + * If existing text should be replaced, the existing text must be a prefix of the text to insert. + */ + text: string; + + /** + * The range to replace. + * Must begin and end on the same line. + * + * Prefer replacements over insertions to avoid cache invalidation. + * Instead of reporting a completion that extends a word, + * the whole word should be replaced with the extended word. + */ + range?: Range; + + /** + * An optional {@link Command} that is executed *after* inserting this completion. + */ + command?: Command; + + constructor(text: string, range?: Range, command?: Command); + } + + + /** + * Be aware that this API will not ever be finalized. + */ + export namespace window { + export function getInlineCompletionItemController(provider: InlineCompletionItemProvider): InlineCompletionController; + } + + /** + * Be aware that this API will not ever be finalized. + */ + export interface InlineCompletionController { + /** + * Is fired when an inline completion item is shown to the user. + */ + // eslint-disable-next-line vscode-dts-event-naming + readonly onDidShowCompletionItem: Event>; + } + + /** + * Be aware that this API will not ever be finalized. + */ + export interface InlineCompletionItemDidShowEvent { + completionItem: T; + } + + //#endregion + + //#region FileSystemProvider stat readonly - https://github.com/microsoft/vscode/issues/73122 + + export enum FilePermission { + /** + * The file is readonly. + * + * *Note:* All `FileStat` from a `FileSystemProvider` that is registered with + * the option `isReadonly: true` will be implicitly handled as if `FilePermission.Readonly` + * is set. As a consequence, it is not possible to have a readonly file system provider + * registered where some `FileStat` are not readonly. + */ + Readonly = 1 + } + + /** + * The `FileStat`-type represents metadata about a file + */ + export interface FileStat { + + /** + * The permissions of the file, e.g. whether the file is readonly. + * + * *Note:* This value might be a bitmask, e.g. `FilePermission.Readonly | FilePermission.Other`. + */ + permissions?: FilePermission; + } + + //#endregion + + //#region https://github.com/microsoft/vscode/issues/87110 @eamodio + + export interface Memento { + + /** + * The stored keys. + */ + readonly keys: readonly string[]; + } + + //#endregion } diff --git a/src/vs/workbench/api/browser/apiCommands.ts b/src/vs/workbench/api/browser/apiCommands.ts index a41b0c30d7..fb266a4283 100644 --- a/src/vs/workbench/api/browser/apiCommands.ts +++ b/src/vs/workbench/api/browser/apiCommands.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index c6f78512ae..a48360d670 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -65,6 +65,7 @@ import './mainThreadComments'; import './mainThreadNotebook'; import './mainThreadNotebookKernels'; import './mainThreadNotebookDocumentsAndEditors'; +import './mainThreadNotebookRenderers'; import './mainThreadTask'; import './mainThreadLabelService'; import './mainThreadTunnelService'; diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 055a5b00c1..0fe71dadac 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -34,12 +34,14 @@ export class MainThreadAuthenticationProvider extends Disposable { const allowedExtensions = readAllowedExtensions(this.storageService, this.id, accountName); if (!allowedExtensions.length) { - this.dialogService.show(Severity.Info, nls.localize('noTrustedExtensions', "This account has not been used by any extensions."), []); + this.dialogService.show(Severity.Info, nls.localize('noTrustedExtensions', "This account has not been used by any extensions.")); return; } const quickPick = this.quickInputService.createQuickPick<{ label: string, description: string, extension: AllowedExtension }>(); quickPick.canSelectMany = true; + quickPick.customButton = true; + quickPick.customLabel = nls.localize('manageTrustedExtensions.cancel', 'Cancel'); const usages = readAccountUsages(this.storageService, this.id, accountName); const items = allowedExtensions.map(extension => { const usage = usages.find(usage => extension.id === usage.extensionId); @@ -68,6 +70,10 @@ export class MainThreadAuthenticationProvider extends Disposable { quickPick.dispose(); }); + quickPick.onDidCustom(() => { + quickPick.hide(); + }); + quickPick.show(); } diff --git a/src/vs/workbench/api/browser/mainThreadCLICommands.ts b/src/vs/workbench/api/browser/mainThreadCLICommands.ts index aea0820c21..20497c7c0f 100644 --- a/src/vs/workbench/api/browser/mainThreadCLICommands.ts +++ b/src/vs/workbench/api/browser/mainThreadCLICommands.ts @@ -54,7 +54,7 @@ CommandsRegistry.registerCommand('_remoteCLI.manageExtensions', async function ( const extensionManagementServerService = accessor.get(IExtensionManagementServerService); const remoteExtensionManagementService = extensionManagementServerService.remoteExtensionManagementServer?.extensionManagementService; if (!remoteExtensionManagementService) { - return undefined; + return undefined; // {{SQL CARBON EDIT}} Strict nulls } const cliService = instantiationService.createChild(new ServiceCollection([IExtensionManagementService, remoteExtensionManagementService])).createInstance(RemoteExtensionCLIManagementService); diff --git a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts index de078d8563..bca89675ff 100644 --- a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { multibyteAwareBtoa } from 'vs/base/browser/dom'; -import { CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async'; +import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors'; @@ -16,7 +16,7 @@ import { isEqual, isEqualOrParent, toLocalResource } from 'vs/base/common/resour import { URI, UriComponents } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { FileChangesEvent, FileChangeType, FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files'; +import { FileOperation, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { IUndoRedoService, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; @@ -35,9 +35,10 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; -import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { IWorkingCopyFileService, WorkingCopyFileEvent } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IWorkingCopy, IWorkingCopyBackup, NO_TYPE_ID, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { ResourceWorkingCopy } from 'vs/workbench/services/workingCopy/common/resourceWorkingCopy'; const enum CustomEditorModelType { Custom, @@ -50,6 +51,8 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc private readonly _editorProviders = new Map(); + private readonly _editorRenameBackups = new Map(); + constructor( context: extHostProtocol.IExtHostContext, private readonly mainThreadWebview: MainThreadWebviews, @@ -60,7 +63,7 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc @ICustomEditorService private readonly _customEditorService: ICustomEditorService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, @IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService, - @IInstantiationService private readonly _instantiationService: IInstantiationService + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); @@ -89,6 +92,9 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc }, resolveWebview: () => { throw new Error('not implemented'); } })); + + // Working copy operations + this._register(workingCopyFileService.onWillRunWorkingCopyFileOperation(async e => this.onWillRunWorkingCopyFileOperation(e))); } override dispose() { @@ -137,9 +143,18 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc webviewInput.webview.options = options; webviewInput.webview.extension = extension; + // If there's an old resource this was a move and we must resolve the backup at the same time as the webview + // This is because the backup must be ready upon model creation, and the input resolve method comes after + let backupId = webviewInput.backupId; + if (webviewInput.oldResource && !webviewInput.backupId) { + const backup = this._editorRenameBackups.get(webviewInput.oldResource.toString()); + backupId = backup?.backupId; + this._editorRenameBackups.delete(webviewInput.oldResource.toString()); + } + let modelRef: IReference; try { - modelRef = await this.getOrCreateCustomEditorModel(modelType, resource, viewType, { backupId: webviewInput.backupId }, cancellation); + modelRef = await this.getOrCreateCustomEditorModel(modelType, resource, viewType, { backupId }, cancellation); } catch (error) { onUnexpectedError(error); webviewInput.webview.html = this.mainThreadWebview.getWebviewResolvedFailedContent(viewType); @@ -252,6 +267,31 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc } return model; } + + //#region Working Copy + private async onWillRunWorkingCopyFileOperation(e: WorkingCopyFileEvent) { + if (e.operation !== FileOperation.MOVE) { + return; + } + e.waitUntil((async () => { + const models = []; + for (const file of e.files) { + if (file.source) { + models.push(...(await this._customEditorService.models.getAllModels(file.source))); + } + } + for (const model of models) { + if (model instanceof MainThreadCustomEditorModel && model.isDirty()) { + const workingCopy = await model.backup(CancellationToken.None); + if (workingCopy.meta) { + // This cast is safe because we do an instanceof check above and a custom document backup data is always returned + this._editorRenameBackups.set(model.editorResource.toString(), workingCopy.meta as CustomDocumentBackupData); + } + } + } + })()); + } + //#endregion } namespace HotExitState { @@ -276,9 +316,7 @@ namespace HotExitState { } -class MainThreadCustomEditorModel extends Disposable implements ICustomEditorModel, IWorkingCopy { - - #isDisposed = false; +class MainThreadCustomEditorModel extends ResourceWorkingCopy implements ICustomEditorModel { private _fromBackup: boolean = false; private _hotExitState: HotExitState.State = HotExitState.Allowed; @@ -288,13 +326,9 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod private _savePoint: number = -1; private readonly _edits: Array = []; private _isDirtyFromContentChange = false; - private _inOrphaned = false; private _ongoingSave?: CancelablePromise; - private readonly _onDidChangeOrphaned = this._register(new Emitter()); - public readonly onDidChangeOrphaned = this._onDidChangeOrphaned.event; - // TODO@mjbvz consider to enable a `typeId` that is specific for custom // editors. Using a distinct `typeId` allows the working copy to have // any resource (including file based resources) even if other working @@ -322,7 +356,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod untitledDocumentData = editors[0].untitledDocumentData; } const { editable } = await proxy.$createCustomDocument(resource, viewType, options.backupId, untitledDocumentData, cancellation); - return instantiationService.createInstance(MainThreadCustomEditorModel, proxy, viewType, resource, !!options.backupId, editable, getEditors); + return instantiationService.createInstance(MainThreadCustomEditorModel, proxy, viewType, resource, !!options.backupId, editable, !!untitledDocumentData, getEditors); } constructor( @@ -331,16 +365,17 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod private readonly _editorResource: URI, fromBackup: boolean, private readonly _editable: boolean, + startDirty: boolean, private readonly _getEditors: () => CustomEditorInput[], @IFileDialogService private readonly _fileDialogService: IFileDialogService, - @IFileService private readonly _fileService: IFileService, + @IFileService fileService: IFileService, @ILabelService private readonly _labelService: ILabelService, @IUndoRedoService private readonly _undoService: IUndoRedoService, @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @IWorkingCopyService workingCopyService: IWorkingCopyService, - @IPathService private readonly _pathService: IPathService + @IPathService private readonly _pathService: IPathService, ) { - super(); + super(MainThreadCustomEditorModel.toWorkingCopyResource(_viewType, _editorResource), fileService); this._fromBackup = fromBackup; @@ -348,7 +383,10 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod this._register(workingCopyService.registerWorkingCopy(this)); } - this._register(_fileService.onDidFilesChange(e => this.onDidFilesChange(e))); + // Normally means we're re-opening an untitled file + if (startDirty) { + this._isDirtyFromContentChange = true; + } } get editorResource() { @@ -356,8 +394,6 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod } override dispose() { - this.#isDisposed = true; - if (this._editable) { this._undoService.removeElements(this._editorResource); } @@ -369,11 +405,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod //#region IWorkingCopy - public get resource() { - // Make sure each custom editor has a unique resource for backup and edits - return MainThreadCustomEditorModel.toWorkingCopyResource(this._viewType, this._editorResource); - } - + // Make sure each custom editor has a unique resource for backup and edits private static toWorkingCopyResource(viewType: string, resource: URI) { const authority = viewType.replace(/[^a-z0-9\-_]/gi, '-'); const path = `/${multibyteAwareBtoa(resource.with({ query: null, fragment: null }).toString(true))}`; @@ -403,10 +435,6 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod return this._fromBackup; } - public isOrphaned(): boolean { - return this._inOrphaned; - } - private isUntitled() { return this._editorResource.scheme === Schemas.untitled; } @@ -417,66 +445,12 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); readonly onDidChangeContent: Event = this._onDidChangeContent.event; + readonly onDidChangeReadonly = Event.None; + //#endregion - private async onDidFilesChange(e: FileChangesEvent): Promise { - let fileEventImpactsModel = false; - let newInOrphanModeGuess: boolean | undefined; - - // If we are currently orphaned, we check if the model file was added back - if (this._inOrphaned) { - const modelFileAdded = e.contains(this.editorResource, FileChangeType.ADDED); - if (modelFileAdded) { - newInOrphanModeGuess = false; - fileEventImpactsModel = true; - } - } - - // Otherwise we check if the model file was deleted - else { - const modelFileDeleted = e.contains(this.editorResource, FileChangeType.DELETED); - if (modelFileDeleted) { - newInOrphanModeGuess = true; - fileEventImpactsModel = true; - } - } - - if (fileEventImpactsModel && this._inOrphaned !== newInOrphanModeGuess) { - let newInOrphanModeValidated: boolean = false; - if (newInOrphanModeGuess) { - // We have received reports of users seeing delete events even though the file still - // exists (network shares issue: https://github.com/microsoft/vscode/issues/13665). - // Since we do not want to mark the model as orphaned, we have to check if the - // file is really gone and not just a faulty file event. - await timeout(100); - - if (this.#isDisposed) { - newInOrphanModeValidated = true; - } else { - const exists = await this._fileService.exists(this.editorResource); - newInOrphanModeValidated = !exists; - } - } - - if (this._inOrphaned !== newInOrphanModeValidated && !this.#isDisposed) { - this.setOrphaned(newInOrphanModeValidated); - } - } - } - - private setOrphaned(orphaned: boolean): void { - if (this._inOrphaned !== orphaned) { - this._inOrphaned = orphaned; - this._onDidChangeOrphaned.fire(); - } - } - - public isEditable(): boolean { - return this._editable; - } - - public isOnReadonlyFileSystem(): boolean { - return this._fileService.hasCapability(this.editorResource, FileSystemProviderCapabilities.Readonly); + public isReadonly(): boolean { + return !this._editable; } public get viewType() { @@ -569,7 +543,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod } } - public async revert(_options?: IRevertOptions) { + public async revert(options?: IRevertOptions) { if (!this._editable) { return; } @@ -578,7 +552,10 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod return; } - this._proxy.$revert(this._editorResource, this.viewType, CancellationToken.None); + if (!options?.soft) { + this._proxy.$revert(this._editorResource, this.viewType, CancellationToken.None); + } + this.change(() => { this._isDirtyFromContentChange = false; this._fromBackup = false; @@ -650,7 +627,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod return true; } else { // Since the editor is readonly, just copy the file over - await this._fileService.copy(resource, targetResource, false /* overwrite */); + await this.fileService.copy(resource, targetResource, false /* overwrite */); return true; } } @@ -698,6 +675,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod pendingState.operation.cancel(); }); + let errorMessage = ''; try { const backupId = await pendingState.operation; // Make sure state has not changed in the meantime @@ -716,12 +694,15 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod if (this._hotExitState === pendingState) { this._hotExitState = HotExitState.NotAllowed; } + if (e.message) { + errorMessage = e.message; + } } if (this._hotExitState === HotExitState.Allowed) { return backupData; } - throw new Error('Cannot back up in this state'); + throw new Error(`Cannot back up in this state: ${errorMessage}`); } } diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index 174555ee1c..4a2a4dc2a9 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -329,7 +329,8 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb type: session.configuration.type, name: session.name, folderUri: session.root ? session.root.uri : undefined, - configuration: session.configuration + configuration: session.configuration, + parent: session.parentSession?.getId(), }; } } diff --git a/src/vs/workbench/api/browser/mainThreadDocuments.ts b/src/vs/workbench/api/browser/mainThreadDocuments.ts index 3024491a3f..7524b7295b 100644 --- a/src/vs/workbench/api/browser/mainThreadDocuments.ts +++ b/src/vs/workbench/api/browser/mainThreadDocuments.ts @@ -20,6 +20,7 @@ import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/commo import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { Emitter } from 'vs/base/common/event'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { ResourceMap } from 'vs/base/common/map'; export class BoundModelReferenceCollection { @@ -105,52 +106,39 @@ export class MainThreadDocuments extends Disposable implements MainThreadDocumen private _onIsCaughtUpWithContentChanges = this._register(new Emitter()); public readonly onIsCaughtUpWithContentChanges = this._onIsCaughtUpWithContentChanges.event; - private readonly _modelService: IModelService; - private readonly _textModelResolverService: ITextModelService; - private readonly _textFileService: ITextFileService; - private readonly _fileService: IFileService; - private readonly _environmentService: IWorkbenchEnvironmentService; - private readonly _uriIdentityService: IUriIdentityService; - - private _modelTrackers: { [modelUrl: string]: ModelTracker; }; private readonly _proxy: ExtHostDocumentsShape; - private readonly _modelIsSynced = new Set(); + private readonly _modelTrackers = new ResourceMap(); + private readonly _modelIsSynced = new ResourceMap(); private readonly _modelReferenceCollection: BoundModelReferenceCollection; constructor( documentsAndEditors: MainThreadDocumentsAndEditors, extHostContext: IExtHostContext, - @IModelService modelService: IModelService, - @ITextFileService textFileService: ITextFileService, - @IFileService fileService: IFileService, - @ITextModelService textModelResolverService: ITextModelService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - @IUriIdentityService uriIdentityService: IUriIdentityService, + @IModelService private readonly _modelService: IModelService, + @ITextFileService private readonly _textFileService: ITextFileService, + @IFileService private readonly _fileService: IFileService, + @ITextModelService private readonly _textModelResolverService: ITextModelService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, + @IUriIdentityService private readonly _uriIdentityService: IUriIdentityService, @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, @IPathService private readonly _pathService: IPathService ) { super(); - this._modelService = modelService; - this._textModelResolverService = textModelResolverService; - this._textFileService = textFileService; - this._fileService = fileService; - this._environmentService = environmentService; - this._uriIdentityService = uriIdentityService; - this._modelReferenceCollection = this._register(new BoundModelReferenceCollection(uriIdentityService.extUri)); + this._modelReferenceCollection = this._register(new BoundModelReferenceCollection(_uriIdentityService.extUri)); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocuments); this._register(documentsAndEditors.onDocumentAdd(models => models.forEach(this._onModelAdded, this))); this._register(documentsAndEditors.onDocumentRemove(urls => urls.forEach(this._onModelRemoved, this))); - this._register(modelService.onModelModeChanged(this._onModelModeChanged, this)); + this._register(_modelService.onModelModeChanged(this._onModelModeChanged, this)); - this._register(textFileService.files.onDidSave(e => { + this._register(_textFileService.files.onDidSave(e => { if (this._shouldHandleFileEvent(e.model.resource)) { this._proxy.$acceptModelSaved(e.model.resource); } })); - this._register(textFileService.files.onDidChangeDirty(m => { + this._register(_textFileService.files.onDidChangeDirty(m => { if (this._shouldHandleFileEvent(m.resource)) { this._proxy.$acceptDirtyStateChanged(m.resource, m.isDirty()); } @@ -167,22 +155,18 @@ export class MainThreadDocuments extends Disposable implements MainThreadDocumen } } })); - - this._modelTrackers = Object.create(null); } public override dispose(): void { - Object.keys(this._modelTrackers).forEach((modelUrl) => { - this._modelTrackers[modelUrl].dispose(); - }); - this._modelTrackers = Object.create(null); + dispose(this._modelTrackers.values()); + this._modelTrackers.clear(); super.dispose(); } public isCaughtUpWithContentChanges(resource: URI): boolean { - const modelUrl = resource.toString(); - if (this._modelTrackers[modelUrl]) { - return this._modelTrackers[modelUrl].isCaughtUpWithContentChanges(); + const tracker = this._modelTrackers.get(resource); + if (tracker) { + return tracker.isCaughtUpWithContentChanges(); } return true; } @@ -198,28 +182,25 @@ export class MainThreadDocuments extends Disposable implements MainThreadDocumen // don't synchronize too large models return; } - const modelUrl = model.uri; - this._modelIsSynced.add(modelUrl.toString()); - this._modelTrackers[modelUrl.toString()] = new ModelTracker(model, this._onIsCaughtUpWithContentChanges, this._proxy, this._textFileService); + this._modelIsSynced.set(model.uri, undefined); + this._modelTrackers.set(model.uri, new ModelTracker(model, this._onIsCaughtUpWithContentChanges, this._proxy, this._textFileService)); } private _onModelModeChanged(event: { model: ITextModel; oldModeId: string; }): void { let { model } = event; - const modelUrl = model.uri; - if (!this._modelIsSynced.has(modelUrl.toString())) { + if (!this._modelIsSynced.has(model.uri)) { return; } this._proxy.$acceptModelModeChanged(model.uri, model.getLanguageIdentifier().language); } private _onModelRemoved(modelUrl: URI): void { - const strModelUrl = modelUrl.toString(); - if (!this._modelIsSynced.has(strModelUrl)) { + if (!this._modelIsSynced.has(modelUrl)) { return; } - this._modelIsSynced.delete(strModelUrl); - this._modelTrackers[strModelUrl].dispose(); - delete this._modelTrackers[strModelUrl]; + this._modelIsSynced.delete(modelUrl); + this._modelTrackers.get(modelUrl)!.dispose(); + this._modelTrackers.delete(modelUrl); } // --- from extension host process @@ -252,7 +233,7 @@ export class MainThreadDocuments extends Disposable implements MainThreadDocumen return Promise.reject(new Error(`cannot open ${canonicalUri.toString()}`)); } else if (!extUri.isEqual(documentUri, canonicalUri)) { return Promise.reject(new Error(`cannot open ${canonicalUri.toString()}. Detail: Actual document opened as ${documentUri.toString()}`)); - } else if (!this._modelIsSynced.has(canonicalUri.toString())) { + } else if (!this._modelIsSynced.has(canonicalUri)) { return Promise.reject(new Error(`cannot open ${canonicalUri.toString()}. Detail: Files above 50MB cannot be synchronized with extensions.`)); } else { return canonicalUri; @@ -291,7 +272,7 @@ export class MainThreadDocuments extends Disposable implements MainThreadDocumen }).then(model => { const resource = model.resource; - if (!this._modelIsSynced.has(resource.toString())) { + if (!this._modelIsSynced.has(resource)) { throw new Error(`expected URI ${resource.toString()} to have come to LIFE`); } diff --git a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts index 5d743d943b..d728872e85 100644 --- a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts @@ -20,7 +20,7 @@ import { MainThreadTextEditor } from 'vs/workbench/api/browser/mainThreadEditor' import { MainThreadTextEditors } from 'vs/workbench/api/browser/mainThreadEditors'; import { ExtHostContext, ExtHostDocumentsAndEditorsShape, IDocumentsAndEditorsDelta, IExtHostContext, IModelAddedData, ITextEditorAddData, MainContext } from 'vs/workbench/api/common/extHost.protocol'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; -import { IEditorPane } from 'vs/workbench/common/editor'; +import { editorGroupToViewColumn, EditorGroupColumn, IEditorPane } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; @@ -30,7 +30,6 @@ import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/commo import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; -import { editorGroupToViewColumn, EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; import { diffSets, diffMaps } from 'vs/base/common/collections'; @@ -410,7 +409,7 @@ export class MainThreadDocumentsAndEditors { }; } - private _findEditorPosition(editor: MainThreadTextEditor): EditorViewColumn | undefined { + private _findEditorPosition(editor: MainThreadTextEditor): EditorGroupColumn | undefined { for (const editorPane of this._editorService.visibleEditorPanes) { if (editor.matches(editorPane)) { return editorGroupToViewColumn(this._editorGroupService, editorPane.group); diff --git a/src/vs/workbench/api/browser/mainThreadDownloadService.ts b/src/vs/workbench/api/browser/mainThreadDownloadService.ts index a8a80b8dbd..5fc55b0a21 100644 --- a/src/vs/workbench/api/browser/mainThreadDownloadService.ts +++ b/src/vs/workbench/api/browser/mainThreadDownloadService.ts @@ -23,4 +23,4 @@ export class MainThreadDownloadService extends Disposable implements MainThreadD return this.downloadService.download(URI.revive(uri), URI.revive(to)); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/api/browser/mainThreadEditor.ts b/src/vs/workbench/api/browser/mainThreadEditor.ts index 537cecdbd7..985a8e7d9c 100644 --- a/src/vs/workbench/api/browser/mainThreadEditor.ts +++ b/src/vs/workbench/api/browser/mainThreadEditor.ts @@ -414,7 +414,7 @@ export class MainThreadTextEditor { if (!this._codeEditor) { return; } - this._codeEditor.setDecorations(key, ranges); + this._codeEditor.setDecorations('exthost-api', key, ranges); } public setDecorationsFast(key: string, _ranges: number[]): void { diff --git a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts index eaff99cfc5..9bce4b6ad1 100644 --- a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts +++ b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts @@ -7,8 +7,9 @@ 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 { EditorResourceAccessor, Verbosity } from 'vs/workbench/common/editor'; import { GroupChangeKind, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export interface ITabInfo { name: string; @@ -27,11 +28,12 @@ export class MainThreadEditorTabs { constructor( extHostContext: IExtHostContext, @IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService, + @IEditorService editorService: IEditorService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditorTabs); - this._editorGroupsService.groups.forEach(this._subscribeToGroup, this); + this._editorGroupsService.whenReady.then(() => 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); @@ -41,6 +43,7 @@ export class MainThreadEditorTabs { this._pushEditorTabs(); } })); + this._dispoables.add(editorService.onDidActiveEditorChange(this._pushEditorTabs, this)); this._pushEditorTabs(); } @@ -69,7 +72,8 @@ export class MainThreadEditorTabs { tabs.push({ group: group.id, name: editor.getTitle(Verbosity.SHORT) ?? '', - resource: editor.resource + resource: EditorResourceAccessor.getOriginalUri(editor) ?? editor.resource, + isActive: (this._editorGroupsService.activeGroup === group) && group.isActive(editor) }); } } diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index 09be5aa04c..728bbac61b 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -26,6 +26,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { revive } from 'vs/base/common/marshalling'; import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; export function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] { if (!data?.edits) { @@ -249,10 +250,10 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return Promise.resolve(editor.insertSnippet(template, ranges, opts)); } - $registerTextEditorDecorationType(key: string, options: IDecorationRenderOptions): void { + $registerTextEditorDecorationType(extensionId: ExtensionIdentifier, key: string, options: IDecorationRenderOptions): void { key = `${this._instanceId}-${key}`; this._registeredDecorationTypes[key] = true; - this._codeEditorService.registerDecorationType(key, options); + this._codeEditorService.registerDecorationType(`exthost-api-${extensionId}`, key, options); } $removeTextEditorDecorationType(key: string): void { diff --git a/src/vs/workbench/api/browser/mainThreadExtensionService.ts b/src/vs/workbench/api/browser/mainThreadExtensionService.ts index 7f34457f32..5c62e836a4 100644 --- a/src/vs/workbench/api/browser/mainThreadExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadExtensionService.ts @@ -21,6 +21,7 @@ import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensio import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; import { ITimerService } from 'vs/workbench/services/timer/browser/timerService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { ICommandService } from 'vs/platform/commands/common/commands'; @extHostNamedCustomer(MainContext.MainThreadExtensionService) export class MainThreadExtensionService implements MainThreadExtensionServiceShape { @@ -35,6 +36,7 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha @IHostService private readonly _hostService: IHostService, @IWorkbenchExtensionEnablementService private readonly _extensionEnablementService: IWorkbenchExtensionEnablementService, @ITimerService private readonly _timerService: ITimerService, + @ICommandService private readonly _commandService: ICommandService, @IWorkbenchEnvironmentService protected readonly _environmentService: IWorkbenchEnvironmentService, ) { this._extensionHostKind = extHostContext.extensionHostKind; @@ -105,15 +107,36 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha }); } else { const enablementState = this._extensionEnablementService.getEnablementState(missingInstalledDependency); - this._notificationService.notify({ - severity: Severity.Error, - message: localize('disabledDep', "Cannot activate the '{0}' extension because it depends on the '{1}' extension, which is disabled. Would you like to enable the extension and reload the window?", extName, missingInstalledDependency.manifest.displayName || missingInstalledDependency.manifest.name), - actions: { - primary: [new Action('enable', localize('enable dep', "Enable and Reload"), '', true, - () => this._extensionEnablementService.setEnablement([missingInstalledDependency], enablementState === EnablementState.DisabledGlobally ? EnablementState.EnabledGlobally : EnablementState.EnabledWorkspace) - .then(() => this._hostService.reload(), e => this._notificationService.error(e)))] - } - }); + if (enablementState === EnablementState.DisabledByVirtualWorkspace) { + this._notificationService.notify({ + severity: Severity.Error, + message: localize('notSupportedInWorkspace', "Cannot activate the '{0}' extension because it depends on the '{1}' extension which is not supported in the current workspace", extName, missingInstalledDependency.manifest.displayName || missingInstalledDependency.manifest.name), + }); + } else if (enablementState === EnablementState.DisabledByTrustRequirement) { + this._notificationService.notify({ + severity: Severity.Error, + message: localize('restrictedMode', "Cannot activate the '{0}' extension because it depends on the '{1}' extension which is not supported in Restricted Mode", extName, missingInstalledDependency.manifest.displayName || missingInstalledDependency.manifest.name), + actions: { + primary: [new Action('manageWorkspaceTrust', localize('manageWorkspaceTrust', "Manage Workspace Trust"), '', true, + () => this._commandService.executeCommand('workbench.trust.manage'))] + } + }); + } else if (this._extensionEnablementService.canChangeEnablement(missingInstalledDependency)) { + this._notificationService.notify({ + severity: Severity.Error, + message: localize('disabledDep', "Cannot activate the '{0}' extension because it depends on the '{1}' extension which is disabled. Would you like to enable the extension and reload the window?", extName, missingInstalledDependency.manifest.displayName || missingInstalledDependency.manifest.name), + actions: { + primary: [new Action('enable', localize('enable dep', "Enable and Reload"), '', true, + () => this._extensionEnablementService.setEnablement([missingInstalledDependency], enablementState === EnablementState.DisabledGlobally ? EnablementState.EnabledGlobally : EnablementState.EnabledWorkspace) + .then(() => this._hostService.reload(), e => this._notificationService.error(e)))] + } + }); + } else { + this._notificationService.notify({ + severity: Severity.Error, + message: localize('disabledDepNoAction', "Cannot activate the '{0}' extension because it depends on the '{1}' extension which is disabled.", extName, missingInstalledDependency.manifest.displayName || missingInstalledDependency.manifest.name), + }); + } } } diff --git a/src/vs/workbench/api/browser/mainThreadFileSystem.ts b/src/vs/workbench/api/browser/mainThreadFileSystem.ts index cf4d502d05..cdf97ed1ff 100644 --- a/src/vs/workbench/api/browser/mainThreadFileSystem.ts +++ b/src/vs/workbench/api/browser/mainThreadFileSystem.ts @@ -6,7 +6,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { FileWriteOptions, FileSystemProviderCapabilities, IFileChange, IFileService, IStat, IWatchOptions, FileType, FileOverwriteOptions, FileDeleteOptions, FileOpenOptions, IFileStat, FileOperationError, FileOperationResult, FileSystemProviderErrorCode, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileFolderCopyCapability } from 'vs/platform/files/common/files'; +import { FileWriteOptions, FileSystemProviderCapabilities, IFileChange, IFileService, IStat, IWatchOptions, FileType, FileOverwriteOptions, FileDeleteOptions, FileOpenOptions, IFileStat, FileOperationError, FileOperationResult, FileSystemProviderErrorCode, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileFolderCopyCapability, FilePermission } from 'vs/platform/files/common/files'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ExtHostContext, ExtHostFileSystemShape, IExtHostContext, IFileChangeDto, MainContext, MainThreadFileSystemShape } from '../common/extHost.protocol'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -65,6 +65,7 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape { ctime: stat.ctime, mtime: stat.mtime, size: stat.size, + permissions: MainThreadFileSystem._asFilePermission(stat), type: MainThreadFileSystem._asFileType(stat) }; }).catch(MainThreadFileSystem._handleError); @@ -95,6 +96,13 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape { return res; } + private static _asFilePermission(stat: IFileStat): FilePermission | undefined { + if (stat.readonly) { + return FilePermission.Readonly; + } + return undefined; + } + $readFile(uri: UriComponents): Promise { return this._fileService.readFile(URI.revive(uri)).then(file => file.value).catch(MainThreadFileSystem._handleError); } diff --git a/src/vs/workbench/api/browser/mainThreadLabelService.ts b/src/vs/workbench/api/browser/mainThreadLabelService.ts index e475995fc8..0fc60aa4a4 100644 --- a/src/vs/workbench/api/browser/mainThreadLabelService.ts +++ b/src/vs/workbench/api/browser/mainThreadLabelService.ts @@ -33,4 +33,4 @@ export class MainThreadLabelService implements MainThreadLabelServiceShape { dispose(): void { // noop } -} \ No newline at end of file +} diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 1cd8c42e40..ed4d0ed2d1 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -11,7 +11,7 @@ import * as search from 'vs/workbench/contrib/search/common/search'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Position as EditorPosition } from 'vs/editor/common/core/position'; import { Range as EditorRange, IRange } from 'vs/editor/common/core/range'; -import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ILanguageConfigurationDto, IRegExpDto, IIndentationRuleDto, IOnEnterRuleDto, ILocationDto, IWorkspaceSymbolDto, reviveWorkspaceEditDto, IDocumentFilterDto, IDefinitionLinkDto, ISignatureHelpProviderMetadataDto, ILinkDto, ICallHierarchyItemDto, ISuggestDataDto, ICodeActionDto, ISuggestDataDtoField, ISuggestResultDtoField, ICodeActionProviderMetadataDto, ILanguageWordDefinitionDto } from '../common/extHost.protocol'; +import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ILanguageConfigurationDto, IRegExpDto, IIndentationRuleDto, IOnEnterRuleDto, ILocationDto, IWorkspaceSymbolDto, reviveWorkspaceEditDto, IDocumentFilterDto, IDefinitionLinkDto, ISignatureHelpProviderMetadataDto, ILinkDto, ICallHierarchyItemDto, ISuggestDataDto, ICodeActionDto, ISuggestDataDtoField, ISuggestResultDtoField, ICodeActionProviderMetadataDto, ILanguageWordDefinitionDto, IdentifiableInlineCompletions, IdentifiableInlineCompletion } from '../common/extHost.protocol'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { LanguageConfiguration, IndentationRule, OnEnterRule } from 'vs/editor/common/modes/languageConfiguration'; import { IModeService } from 'vs/editor/common/services/modeService'; @@ -471,7 +471,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha provideCompletionItems: async (model: ITextModel, position: EditorPosition, context: modes.CompletionContext, token: CancellationToken): Promise => { const result = await this._proxy.$provideCompletionItems(handle, model.uri, position, context, token); if (!result) { - return result; + return result; // {{SQL CARBON EDIT}} } return { suggestions: result[ISuggestResultDtoField.completions].map(d => MainThreadLanguageFeatures._inflateSuggestDto(result[ISuggestResultDtoField.defaultRanges], d)), @@ -500,6 +500,21 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha this._registrations.set(handle, modes.CompletionProviderRegistry.register(selector, provider)); } + $registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[]): void { + const provider: modes.InlineCompletionsProvider = { + provideInlineCompletions: async (model: ITextModel, position: EditorPosition, context: modes.InlineCompletionContext, token: CancellationToken): Promise => { + return this._proxy.$provideInlineCompletions(handle, model.uri, position, context, token); + }, + handleItemDidShow: async (completions: IdentifiableInlineCompletions, item: IdentifiableInlineCompletion): Promise => { + return this._proxy.$handleInlineCompletionDidShow(handle, completions.pid, item.idx); + }, + freeInlineCompletions: (completions: IdentifiableInlineCompletions): void => { + this._proxy.$freeInlineCompletionsList(handle, completions.pid); + } + }; + this._registrations.set(handle, modes.InlineCompletionsProviderRegistry.register(selector, provider)); + } + // --- parameter hints $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void { @@ -525,10 +540,10 @@ 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); + $registerInlayHintsProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void { + const provider = { + provideInlayHints: async (model: ITextModel, range: EditorRange, token: CancellationToken): Promise => { + const result = await this._proxy.$provideInlayHints(handle, model.uri, range, token); return result?.hints; } }; @@ -536,13 +551,13 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha if (typeof eventHandle === 'number') { const emitter = new Emitter(); this._registrations.set(eventHandle, emitter); - provider.onDidChangeInlineHints = emitter.event; + provider.onDidChangeInlayHints = emitter.event; } - this._registrations.set(handle, modes.InlineHintsProviderRegistry.register(selector, provider)); + this._registrations.set(handle, modes.InlayHintsProviderRegistry.register(selector, provider)); } - $emitInlineHintsEvent(eventHandle: number, event?: any): void { + $emitInlayHintsEvent(eventHandle: number, event?: any): void { const obj = this._registrations.get(eventHandle); if (obj instanceof Emitter) { obj.fire(event); diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 15baec2b74..55cef7622d 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -6,13 +6,11 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; -import { IRelativePattern } from 'vs/base/common/glob'; import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; -import { NotebookSelector } from 'vs/workbench/contrib/notebook/common/notebookSelector'; -import { INotebookCellStatusBarItemProvider, INotebookExclusiveDocumentFilter, NotebookDataDto, TransientCellMetadata, TransientDocumentMetadata, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookCellStatusBarItemProvider, INotebookContributionData, NotebookDataDto, TransientCellMetadata, TransientDocumentMetadata, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookContentProvider, INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { ExtHostContext, ExtHostNotebookShape, IExtHostContext, MainContext, MainThreadNotebookShape, NotebookExtensionDescription } from '../common/extHost.protocol'; @@ -43,13 +41,8 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { dispose(this._notebookSerializer.values()); } - async $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, options: { - transientOutputs: boolean; - transientCellMetadata: TransientCellMetadata; - transientDocumentMetadata: TransientDocumentMetadata; - viewOptions?: { displayName: string; filenamePattern: (string | IRelativePattern | INotebookExclusiveDocumentFilter)[]; exclusive: boolean; }; - }): Promise { - let contentOptions = { transientOutputs: options.transientOutputs, transientCellMetadata: options.transientCellMetadata, transientDocumentMetadata: options.transientDocumentMetadata }; + async $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, options: TransientOptions, data: INotebookContributionData | undefined): Promise { + let contentOptions = { ...options }; const controller: INotebookContentProvider = { get options() { @@ -60,7 +53,6 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { contentOptions.transientDocumentMetadata = newOptions.transientDocumentMetadata; contentOptions.transientOutputs = newOptions.transientOutputs; }, - viewOptions: options.viewOptions, open: async (uri: URI, backupId: string | undefined, untitledDocumentData: VSBuffer | undefined, token: CancellationToken) => { const data = await this._proxy.$openNotebook(viewType, uri, backupId, untitledDocumentData, token); return { @@ -79,7 +71,11 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { } }; - const disposable = this._notebookService.registerNotebookController(viewType, extension, controller); + const disposable = new DisposableStore(); + disposable.add(this._notebookService.registerNotebookController(viewType, extension, controller)); + if (data) { + disposable.add(this._notebookService.registerContributedNotebookType(viewType, data)); + } this._notebookProviders.set(viewType, { controller, disposable }); } @@ -104,7 +100,7 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { } } - $registerNotebookSerializer(handle: number, extension: NotebookExtensionDescription, viewType: string, options: TransientOptions): void { + $registerNotebookSerializer(handle: number, extension: NotebookExtensionDescription, viewType: string, options: TransientOptions, data: INotebookContributionData | undefined): void { const registration = this._notebookService.registerNotebookSerializer(viewType, extension, { options, dataToNotebook: (data: VSBuffer): Promise => { @@ -114,7 +110,12 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { return this._proxy.$notebookToData(handle, data, CancellationToken.None); } }); - this._notebookSerializer.set(handle, registration); + const disposables = new DisposableStore(); + disposables.add(registration); + if (data) { + disposables.add(this._notebookService.registerContributedNotebookType(viewType, data)); + } + this._notebookSerializer.set(handle, disposables); } $unregisterNotebookSerializer(handle: number): void { @@ -129,7 +130,7 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { } } - async $registerNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined, selector: NotebookSelector): Promise { + async $registerNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined, viewType: string): Promise { const that = this; const provider: INotebookCellStatusBarItemProvider = { async provideCellStatusBarItems(uri: URI, index: number, token: CancellationToken) { @@ -143,7 +144,7 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { } }; }, - selector: selector + viewType }; if (typeof eventHandle === 'number') { diff --git a/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts b/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts index d2bf4a13f2..87e779f15f 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; @@ -9,19 +9,21 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { BoundModelReferenceCollection } from 'vs/workbench/api/browser/mainThreadDocuments'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { IImmediateCellEditOperation, IMainCellDto, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IImmediateCellEditOperation, IMainCellDto, NotebookCellsChangeType, NotebookDataDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; -import { ExtHostContext, ExtHostNotebookShape, IExtHostContext, MainThreadNotebookDocumentsShape } from '../common/extHost.protocol'; +import { ExtHostContext, ExtHostNotebookDocumentsShape, IExtHostContext, MainThreadNotebookDocumentsShape } from '../common/extHost.protocol'; import { MainThreadNotebooksAndEditors } from 'vs/workbench/api/browser/mainThreadNotebookDocumentsAndEditors'; import { onUnexpectedError } from 'vs/base/common/errors'; +import { Schemas } from 'vs/base/common/network'; +import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsShape { private readonly _disposables = new DisposableStore(); - private readonly _proxy: ExtHostNotebookShape; + private readonly _proxy: ExtHostNotebookDocumentsShape; private readonly _documentEventListenersMapping = new ResourceMap(); private readonly _modelReferenceCollection: BoundModelReferenceCollection; @@ -32,7 +34,7 @@ export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsS @INotebookEditorModelResolverService private readonly _notebookEditorModelResolverService: INotebookEditorModelResolverService, @IUriIdentityService private readonly _uriIdentityService: IUriIdentityService ) { - this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebook); + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebookDocuments); this._modelReferenceCollection = new BoundModelReferenceCollection(this._uriIdentityService.extUri); notebooksAndEditors.onDidAddNotebooks(this._handleNotebooksAdded, this, this._disposables); @@ -47,7 +49,6 @@ export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsS this._disposables.dispose(); this._modelReferenceCollection.dispose(); dispose(this._documentEventListenersMapping.values()); - } private _handleNotebooksAdded(notebooks: readonly NotebookTextModel[]): void { @@ -114,18 +115,59 @@ export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsS language: cell.language, cellKind: cell.cellKind, outputs: cell.outputs, - metadata: cell.metadata + metadata: cell.metadata, + internalMetadata: cell.internalMetadata, }; } - async $tryOpenDocument(uriComponents: UriComponents): Promise { + async $tryCreateNotebook(options: { viewType: string, content?: NotebookDataDto }): Promise { + + const info = this._notebookService.getContributedNotebookType(options.viewType); + if (!info) { + throw new Error('UNKNOWN view type: ' + options.viewType); + } + + // find a free URI for the untitled case + const suffix = NotebookProviderInfo.possibleFileEnding(info.selectors) ?? ''; + let uri: URI; + for (let counter = 1; ; counter++) { + let candidate = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}${suffix}`, query: options.viewType }); + if (!this._notebookService.getNotebookTextModel(candidate)) { + uri = candidate; + break; + } + } + + const ref = await this._notebookEditorModelResolverService.resolve(uri, options.viewType); + + // untitled notebooks are disposed when they get saved. we should not hold a reference + // to such a disposed notebook and therefore dispose the reference as well + ref.object.notebook.onWillDispose(() => { + ref.dispose(); + }); + + // untitled notebooks are dirty by default + this._proxy.$acceptDirtyStateChanged(uri, true); + + // apply content changes... slightly HACKY -> this triggers a change event + if (options.content) { + ref.object.notebook.reset( + options.content.cells, + options.content.metadata, + ref.object.notebook.transientOptions + ); + } + return uri; + } + + async $tryOpenNotebook(uriComponents: UriComponents): Promise { const uri = URI.revive(uriComponents); const ref = await this._notebookEditorModelResolverService.resolve(uri, undefined); this._modelReferenceCollection.add(uri, ref); return uri; } - async $trySaveDocument(uriComponents: UriComponents) { + async $trySaveNotebook(uriComponents: UriComponents) { const uri = URI.revive(uriComponents); const ref = await this._notebookEditorModelResolverService.resolve(uri); diff --git a/src/vs/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts b/src/vs/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts index c0f0aff2d5..034f61245c 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { diffMaps, diffSets } from 'vs/base/common/collections'; @@ -30,7 +30,7 @@ interface INotebookAndEditorDelta { } class NotebookAndEditorState { - static compute(before: NotebookAndEditorState | undefined, after: NotebookAndEditorState): INotebookAndEditorDelta { + static delta(before: NotebookAndEditorState | undefined, after: NotebookAndEditorState): INotebookAndEditorDelta { if (!before) { return { addedDocuments: [...after.documents], @@ -107,7 +107,7 @@ export class MainThreadNotebooksAndEditors { extHostContext.set(MainContext.MainThreadNotebookDocuments, this._mainThreadNotebooks); extHostContext.set(MainContext.MainThreadNotebookEditors, this._mainThreadEditors); - this._notebookService.onDidCreateNotebookDocument(() => this._updateState(), this, this._disposables); + this._notebookService.onWillAddNotebookDocument(() => this._updateState(), this, this._disposables); this._notebookService.onDidRemoveNotebookDocument(() => this._updateState(), this, this._disposables); this._editorService.onDidActiveEditorChange(() => this._updateState(), this, this._disposables); this._editorService.onDidVisibleEditorsChange(() => this._updateState(), this, this._disposables); @@ -170,7 +170,7 @@ export class MainThreadNotebooksAndEditors { } const newState = new NotebookAndEditorState(new Set(this._notebookService.listNotebookDocuments()), editors, activeEditor, visibleEditorsMap); - this._onDelta(NotebookAndEditorState.compute(this._currentState, newState)); + this._onDelta(NotebookAndEditorState.delta(this._currentState, newState)); this._currentState = newState; } @@ -234,7 +234,8 @@ export class MainThreadNotebooksAndEditors { language: cell.language, cellKind: cell.cellKind, outputs: cell.outputs, - metadata: cell.metadata + metadata: cell.metadata, + internalMetadata: cell.internalMetadata, })) }; } diff --git a/src/vs/workbench/api/browser/mainThreadNotebookEditors.ts b/src/vs/workbench/api/browser/mainThreadNotebookEditors.ts index a8ca5a5ac4..30bd4e17da 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookEditors.ts @@ -1,12 +1,12 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; -import { getNotebookEditorFromEditorPane, INotebookEditor, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { getNotebookEditorFromEditorPane, INotebookEditor, INotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; -import { ExtHostContext, ExtHostNotebookShape, IExtHostContext, INotebookDocumentShowOptions, INotebookEditorViewColumnInfo, MainThreadNotebookEditorsShape, NotebookEditorRevealType } from '../common/extHost.protocol'; +import { ExtHostContext, ExtHostNotebookEditorsShape, IExtHostContext, INotebookDocumentShowOptions, INotebookEditorViewColumnInfo, MainThreadNotebookEditorsShape, NotebookEditorRevealType } from '../common/extHost.protocol'; import { MainThreadNotebooksAndEditors } from 'vs/workbench/api/browser/mainThreadNotebookDocumentsAndEditors'; import { ICellEditOperation, INotebookDecorationRenderOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; @@ -36,7 +36,7 @@ export class MainThreadNotebookEditors implements MainThreadNotebookEditorsShape private readonly _disposables = new DisposableStore(); - private readonly _proxy: ExtHostNotebookShape; + private readonly _proxy: ExtHostNotebookEditorsShape; private readonly _mainThreadEditors = new Map(); private _currentViewColumnInfo?: INotebookEditorViewColumnInfo; @@ -50,7 +50,7 @@ export class MainThreadNotebookEditors implements MainThreadNotebookEditorsShape @INotebookEditorService private readonly _notebookEditorService: INotebookEditorService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService ) { - this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebook); + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebookEditors); notebooksAndEditors.onDidAddEditors(this._handleEditorsAdded, this, this._disposables); notebooksAndEditors.onDidRemoveEditors(this._handleEditorsRemoved, this, this._disposables); @@ -121,10 +121,8 @@ export class MainThreadNotebookEditors implements MainThreadNotebookEditorsShape return editor.textModel.applyEdits(cellEdits, true, undefined, () => undefined, undefined); } - - async $tryShowNotebookDocument(resource: UriComponents, viewType: string, options: INotebookDocumentShowOptions): Promise { - const editorOptions = new NotebookEditorOptions({ + const editorOptions: INotebookEditorOptions = { cellSelections: options.selections, preserveFocus: options.preserveFocus, pinned: options.pinned, @@ -132,8 +130,8 @@ export class MainThreadNotebookEditors implements MainThreadNotebookEditorsShape // preserve pre 1.38 behaviour to not make group active when preserveFocus: true // but make sure to restore the editor to fix https://github.com/microsoft/vscode/issues/79633 activation: options.preserveFocus ? EditorActivation.RESTORE : undefined, - override: EditorOverride.DISABLED, - }); + override: EditorOverride.DISABLED + }; const input = NotebookEditorInput.create(this._instantiationService, URI.revive(resource), viewType); const editorPane = await this._editorService.openEditor(input, editorOptions, options.position); @@ -188,4 +186,12 @@ export class MainThreadNotebookEditors implements MainThreadNotebookEditorsShape notebookEditor.setEditorDecorations(key, range); } } + + $trySetSelections(id: string, ranges: ICellRange[]): void { + const editor = this._notebookEditorService.getNotebookEditor(id); + if (editor) { + // @rebornix how to set an editor selection? + // editor.setSelections(ranges) + } + } } diff --git a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts index 308e0964ae..1bd4410626 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { flatten, isNonEmptyArray } from 'vs/base/common/arrays'; @@ -44,7 +44,7 @@ abstract class MainThreadKernel implements INotebookKernel { constructor(data: INotebookKernelDto2, private _modeService: IModeService) { this.id = data.id; - this.viewType = data.viewType; + this.viewType = data.notebookType; this.extension = data.extensionId; this.implementsInterrupt = data.supportsInterrupt ?? false; @@ -52,7 +52,7 @@ abstract class MainThreadKernel implements INotebookKernel { this.description = data.description; this.detail = data.detail; this.supportedLanguages = isNonEmptyArray(data.supportedLanguages) ? data.supportedLanguages : _modeService.getRegisteredModes(); - this.implementsExecutionOrder = data.hasExecutionOrder ?? false; + this.implementsExecutionOrder = data.supportsExecutionOrder ?? false; this.localResourceRoot = URI.revive(data.extensionLocation); this.preloads = data.preloads?.map(u => ({ uri: URI.revive(u.uri), provides: u.provides })) ?? []; } @@ -77,8 +77,8 @@ abstract class MainThreadKernel implements INotebookKernel { this.supportedLanguages = isNonEmptyArray(data.supportedLanguages) ? data.supportedLanguages : this._modeService.getRegisteredModes(); event.supportedLanguages = true; } - if (data.hasExecutionOrder !== undefined) { - this.implementsExecutionOrder = data.hasExecutionOrder; + if (data.supportsExecutionOrder !== undefined) { + this.implementsExecutionOrder = data.supportsExecutionOrder; event.hasExecutionOrder = true; } this._onDidChange.fire(event); @@ -122,9 +122,6 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape private _onEditorAdd(editor: INotebookEditor) { const ipcListener = editor.onDidReceiveMessage(e => { - if (e.forRenderer) { - return; - } if (!editor.hasModel()) { return; } @@ -134,7 +131,7 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape } for (let [handle, candidate] of this._kernels) { if (candidate[0] === selected) { - this._proxy.$acceptRendererMessage(handle, editor.getId(), e.message); + this._proxy.$acceptKernelMessageFromRenderer(handle, editor.getId(), e.message); break; } } @@ -164,11 +161,11 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape } if (editorId === undefined) { // all editors - editor.postMessage(undefined, message); + editor.postMessage(message); didSend = true; } else if (editor.getId() === editorId) { // selected editors - editor.postMessage(undefined, message); + editor.postMessage(message); didSend = true; break; } @@ -188,16 +185,16 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape await that._proxy.$cancelCells(handle, uri, handles); } }(data, this._modeService); - const registration = this._notebookKernelService.registerKernel(kernel); const listener = this._notebookKernelService.onDidChangeNotebookKernelBinding(e => { if (e.oldKernel === kernel.id) { - this._proxy.$acceptSelection(handle, e.notebook, false); + this._proxy.$acceptNotebookAssociation(handle, e.notebook, false); } else if (e.newKernel === kernel.id) { - this._proxy.$acceptSelection(handle, e.notebook, true); + this._proxy.$acceptNotebookAssociation(handle, e.notebook, true); } }); + const registration = this._notebookKernelService.registerKernel(kernel); this._kernels.set(handle, [kernel, combinedDisposable(listener, registration)]); } diff --git a/src/vs/workbench/api/browser/mainThreadNotebookRenderers.ts b/src/vs/workbench/api/browser/mainThreadNotebookRenderers.ts new file mode 100644 index 0000000000..ed723b6f50 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadNotebookRenderers.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ExtHostContext, ExtHostNotebookRenderersShape, IExtHostContext, MainContext, MainThreadNotebookRenderersShape } from 'vs/workbench/api/common/extHost.protocol'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { INotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; + +@extHostNamedCustomer(MainContext.MainThreadNotebookRenderers) +export class MainThreadNotebookRenderers extends Disposable implements MainThreadNotebookRenderersShape { + private readonly proxy: ExtHostNotebookRenderersShape; + + constructor( + extHostContext: IExtHostContext, + @INotebookRendererMessagingService private readonly messaging: INotebookRendererMessagingService, + ) { + super(); + this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebookRenderers); + this._register(messaging.onShouldPostMessage(e => { + this.proxy.$postRendererMessage(e.editorId, e.rendererId, e.message); + })); + } + + $postMessage(editorId: string, rendererId: string, message: unknown): void { + this.messaging.fireDidReceiveMessage(editorId, rendererId, message); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadStatusBar.ts b/src/vs/workbench/api/browser/mainThreadStatusBar.ts index ec4acd28f1..10a0ccdee8 100644 --- a/src/vs/workbench/api/browser/mainThreadStatusBar.ts +++ b/src/vs/workbench/api/browser/mainThreadStatusBar.ts @@ -27,7 +27,7 @@ export class MainThreadStatusBar implements MainThreadStatusBarShape { this.entries.clear(); } - $setEntry(id: number, statusId: string, statusName: string, text: string, tooltip: string | undefined, command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: string | ThemeColor | undefined, alignment: MainThreadStatusBarAlignment, priority: number | undefined, accessibilityInformation: IAccessibilityInformation): void { + $setEntry(entryId: number, id: string, name: string, text: string, tooltip: string | undefined, command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: string | ThemeColor | undefined, alignment: MainThreadStatusBarAlignment, priority: number | undefined, accessibilityInformation: IAccessibilityInformation): void { // if there are icons in the text use the tooltip for the aria label let ariaLabel: string; let role: string | undefined = undefined; @@ -37,23 +37,23 @@ export class MainThreadStatusBar implements MainThreadStatusBarShape { } else { ariaLabel = getCodiconAriaLabel(text); } - const entry: IStatusbarEntry = { text, tooltip, command, color, backgroundColor, ariaLabel, role }; + const entry: IStatusbarEntry = { name, text, tooltip, command, color, backgroundColor, ariaLabel, role }; if (typeof priority === 'undefined') { priority = 0; } // Reset existing entry if alignment or priority changed - let existingEntry = this.entries.get(id); + let existingEntry = this.entries.get(entryId); if (existingEntry && (existingEntry.alignment !== alignment || existingEntry.priority !== priority)) { dispose(existingEntry.accessor); - this.entries.delete(id); + this.entries.delete(entryId); existingEntry = undefined; } // Create new entry if not existing if (!existingEntry) { - this.entries.set(id, { accessor: this.statusbarService.addEntry(entry, statusId, statusName, alignment, priority), alignment, priority }); + this.entries.set(entryId, { accessor: this.statusbarService.addEntry(entry, id, alignment, priority), alignment, priority }); } // Otherwise update diff --git a/src/vs/workbench/api/browser/mainThreadTask.ts b/src/vs/workbench/api/browser/mainThreadTask.ts index ad08507c40..ac3e36c54b 100644 --- a/src/vs/workbench/api/browser/mainThreadTask.ts +++ b/src/vs/workbench/api/browser/mainThreadTask.ts @@ -702,9 +702,6 @@ export class MainThreadTask implements MainThreadTaskShape { }); }); }, - getDefaultShellAndArgs: (): Promise<{ shell: string, args: string[] | string | undefined }> => { - return Promise.resolve(this._proxy.$getDefaultShellAndArgs()); - }, findExecutable: (command: string, cwd?: string, paths?: string[]): Promise => { return this._proxy.$findExecutable(command, cwd, paths); } diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index f7dc7fbe7a..f6ac37251a 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -3,22 +3,23 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { StopWatch } from 'vs/base/common/stopwatch'; +import { DisposableStore, Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, 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'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IShellLaunchConfig, IShellLaunchConfigDto, ITerminalDimensions } from 'vs/platform/terminal/common/terminal'; +import { IShellLaunchConfig, IShellLaunchConfigDto, ITerminalDimensions, TitleEventSource } from 'vs/platform/terminal/common/terminal'; import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; -import { ExtHostContext, ExtHostTerminalServiceShape, IExtHostContext, ITerminalDimensionsDto, MainContext, MainThreadTerminalServiceShape, TerminalIdentifier, TerminalLaunchConfig } from 'vs/workbench/api/common/extHost.protocol'; -import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ITerminalExternalLinkProvider, ITerminalInstance, ITerminalInstanceService, ITerminalLink, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy'; import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { deserializeEnvironmentVariableCollection, serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; -import { IAvailableProfilesRequest as IAvailableProfilesRequest, IDefaultShellAndArgsRequest, IStartExtensionTerminalRequest, ITerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/common/terminal'; -import { ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; +import { IStartExtensionTerminalRequest, ITerminalProcessExtHostProxy, ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { OperatingSystem, OS } from 'vs/base/common/platform'; @extHostNamedCustomer(MainContext.MainThreadTerminalService) export class MainThreadTerminalService implements MainThreadTerminalServiceShape { @@ -30,11 +31,10 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape * 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(); + private readonly _profileProviders = new Map(); private _dataEventTracker: TerminalDataEventTracker | undefined; - private _extHostKind: ExtensionHostKind; /** * A single shared terminal link provider for the exthost. When an ext registers a link * provider, this is registered with the terminal on the renderer side and all links are @@ -43,17 +43,19 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape */ private _linkProvider: IDisposable | undefined; + private _os: OperatingSystem = OS; + constructor( - extHostContext: IExtHostContext, + private readonly _extHostContext: IExtHostContext, @ITerminalService private readonly _terminalService: ITerminalService, @ITerminalInstanceService readonly terminalInstanceService: ITerminalInstanceService, - @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IEnvironmentVariableService private readonly _environmentVariableService: IEnvironmentVariableService, @ILogService private readonly _logService: ILogService, + @ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService, + @IRemoteAgentService remoteAgentService: IRemoteAgentService ) { - this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTerminalService); - this._remoteAuthority = extHostContext.remoteAuthority; + this._proxy = _extHostContext.getProxy(ExtHostContext.ExtHostTerminalService); // ITerminalService listeners this._toDispose.add(_terminalService.onInstanceCreated((instance) => { @@ -61,8 +63,6 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._onInstanceDimensionsChanged(instance); })); - this._extHostKind = extHostContext.extensionHostKind; - this._toDispose.add(_terminalService.onInstanceDisposed(instance => this._onTerminalDisposed(instance))); this._toDispose.add(_terminalService.onInstanceProcessIdReady(instance => this._onTerminalProcessIdReady(instance))); this._toDispose.add(_terminalService.onInstanceDimensionsChanged(instance => this._onInstanceDimensionsChanged(instance))); @@ -70,12 +70,6 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._toDispose.add(_terminalService.onInstanceRequestStartExtensionTerminal(e => this._onRequestStartExtensionTerminal(e))); this._toDispose.add(_terminalService.onActiveInstanceChanged(instance => this._onActiveTerminalChanged(instance ? instance.instanceId : null))); this._toDispose.add(_terminalService.onInstanceTitleChanged(instance => instance && this._onTitleChanged(instance.instanceId, instance.title))); - this._toDispose.add(_terminalService.onRequestAvailableProfiles(e => this._onRequestAvailableProfiles(e))); - - // ITerminalInstanceService listeners - if (terminalInstanceService.onRequestDefaultShellAndArgs) { - this._toDispose.add(terminalInstanceService.onRequestDefaultShellAndArgs(e => this._onRequestDefaultShellAndArgs(e))); - } // Set initial ext host state this._terminalService.terminalInstances.forEach(t => { @@ -94,7 +88,11 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._proxy.$initEnvironmentVariableCollections(serializedCollections); } - this._terminalService.extHostReady(extHostContext.remoteAuthority!); // TODO@Tyriar: remove null assertion + remoteAgentService.getEnvironment().then(async env => { + this._os = env?.os || OS; + this._updateDefaultProfile(); + }); + this._terminalService.onDidChangeAvailableProfiles(() => this._updateDefaultProfile()); } public dispose(): void { @@ -102,6 +100,13 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._linkProvider?.dispose(); } + private async _updateDefaultProfile() { + const remoteAuthority = withNullAsUndefined(this._extHostContext.remoteAuthority); + const defaultProfile = this._terminalProfileResolverService.getDefaultProfile({ remoteAuthority, os: this._os }); + const defaultAutomationProfile = this._terminalProfileResolverService.getDefaultProfile({ remoteAuthority, os: this._os, allowAutomationShell: true }); + this._proxy.$acceptDefaultProfile(...await Promise.all([defaultProfile, defaultAutomationProfile])); + } + private _getTerminalId(id: TerminalIdentifier): number | undefined { if (typeof id === 'number') { return id; @@ -135,9 +140,19 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape : undefined, extHostTerminalId: extHostTerminalId, isFeatureTerminal: launchConfig.isFeatureTerminal, - isExtensionOwnedTerminal: launchConfig.isExtensionOwnedTerminal + isExtensionOwnedTerminal: launchConfig.isExtensionOwnedTerminal, + useShellEnvironment: launchConfig.useShellEnvironment }; - const terminal = this._terminalService.createTerminal(shellLaunchConfig); + let terminal: ITerminalInstance | undefined; + if (launchConfig.isSplitTerminal) { + const activeInstance = this._terminalService.getActiveInstance(); + if (activeInstance) { + terminal = withNullAsUndefined(this._terminalService.splitInstance(activeInstance, shellLaunchConfig)); + } + } + if (!terminal) { + terminal = this._terminalService.createTerminal(shellLaunchConfig); + } this._extHostTerminalIds.set(extHostTerminalId, terminal.instanceId); } @@ -196,6 +211,18 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._terminalService.registerProcessSupport(isSupported); } + public $registerProfileProvider(id: string): void { + // Proxy profile provider requests through the extension host + this._profileProviders.set(id, this._terminalService.registerTerminalProfileProvider(id, { + createContributedTerminalProfile: async (isSplitTerminal) => this._proxy.$createContributedProfileTerminal(id, isSplitTerminal) + })); + } + + public $unregisterProfileProvider(id: string): void { + this._profileProviders.get(id)?.dispose(); + this._profileProviders.delete(id); + } + private _onActiveTerminalChanged(terminalId: number | null): void { this._proxy.$acceptActiveTerminalChanged(terminalId); } @@ -265,7 +292,15 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape } public $sendProcessTitle(terminalId: number, title: string): void { - this._terminalProcessProxies.get(terminalId)?.emitTitle(title); + // Since title events can only come from vscode.Pseudoterminals right now, these are routed + // directly to the instance as API source events such that they will replace the initial + // `name` property provided for the Pseudoterminal. If we support showing both Api and + // Process titles at the same time we may want to pass this through as a Process source + // event. + const instance = this._terminalService.getInstanceFromId(terminalId); + if (instance) { + instance.setTitle(title, TitleEventSource.Api); + } } public $sendProcessData(terminalId: number, data: string): void { @@ -308,28 +343,6 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._getTerminalProcess(terminalId)?.emitLatency(sum / COUNT); } - private _isPrimaryExtHost(): boolean { - // The "primary" ext host is the remote ext host if there is one, otherwise the local - const conn = this._remoteAgentService.getConnection(); - if (conn) { - return this._remoteAuthority === conn.remoteAuthority; - } - return this._extHostKind !== ExtensionHostKind.LocalWebWorker; - } - - private async _onRequestAvailableProfiles(req: IAvailableProfilesRequest): Promise { - if (this._isPrimaryExtHost()) { - req.callback(await this._proxy.$getAvailableProfiles(req.configuredProfilesOnly)); - } - } - - private async _onRequestDefaultShellAndArgs(req: IDefaultShellAndArgsRequest): Promise { - if (this._isPrimaryExtHost()) { - const res = await this._proxy.$getDefaultShellAndArgs(req.useAutomationShell); - req.callback(res.shell, res.args); - } - } - private _getTerminalProcess(terminalId: number): ITerminalProcessExtHostProxy | undefined { const terminal = this._terminalProcessProxies.get(terminalId); if (!terminal) { diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index 467bf5f97f..6c6100a620 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -60,7 +60,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) @@ -73,7 +73,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); diff --git a/src/vs/workbench/api/browser/mainThreadTunnelService.ts b/src/vs/workbench/api/browser/mainThreadTunnelService.ts index 6ef117454f..478bd96749 100644 --- a/src/vs/workbench/api/browser/mainThreadTunnelService.ts +++ b/src/vs/workbench/api/browser/mainThreadTunnelService.ts @@ -41,7 +41,8 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun } private processFindingEnabled(): boolean { - return (!!this.configurationService.getValue(PORT_AUTO_FORWARD_SETTING)) && (this.configurationService.getValue(PORT_AUTO_SOURCE_SETTING) === PORT_AUTO_SOURCE_SETTING_PROCESS); + return (!!this.configurationService.getValue(PORT_AUTO_FORWARD_SETTING) || this.tunnelService.hasTunnelProvider) + && (this.configurationService.getValue(PORT_AUTO_SOURCE_SETTING) === PORT_AUTO_SOURCE_SETTING_PROCESS); } async $setRemoteTunnelService(processId: number): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadWebviews.ts b/src/vs/workbench/api/browser/mainThreadWebviews.ts index 202a8153bf..c9bae9169d 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviews.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviews.ts @@ -14,8 +14,7 @@ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IProductService } from 'vs/platform/product/common/productService'; import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; -import { serializeMessage } from 'vs/workbench/api/common/extHostWebview'; -import { deserializeWebviewMessage } from 'vs/workbench/api/common/extHostWebviewMessaging'; +import { serializeWebviewMessage, deserializeWebviewMessage } from 'vs/workbench/api/common/extHostWebviewMessaging'; import { Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; export class MainThreadWebviews extends Disposable implements extHostProtocol.MainThreadWebviewsShape { @@ -74,7 +73,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma disposables.add(webview.onDidClickLink((uri) => this.onDidClickLink(handle, uri))); disposables.add(webview.onMessage((message) => { - const serialized = serializeMessage(message.message, options); + const serialized = serializeWebviewMessage(message.message, options); this._proxy.$onMessage(handle, serialized.message, ...serialized.buffers); })); diff --git a/src/vs/workbench/api/browser/mainThreadWindow.ts b/src/vs/workbench/api/browser/mainThreadWindow.ts index fa94e3599e..1614ac1462 100644 --- a/src/vs/workbench/api/browser/mainThreadWindow.ts +++ b/src/vs/workbench/api/browser/mainThreadWindow.ts @@ -60,8 +60,7 @@ export class MainThreadWindow implements MainThreadWindowShape { } async $asExternalUri(uriComponents: UriComponents, options: IOpenUriOptions): Promise { - const uri = URI.revive(uriComponents); - const result = await this.openerService.resolveExternalUri(uri, options); + const result = await this.openerService.resolveExternalUri(URI.revive(uriComponents), options); return result.resolved; } } diff --git a/src/vs/workbench/api/common/apiCommands.ts b/src/vs/workbench/api/common/apiCommands.ts index 017958bd7e..de0fa1545b 100644 --- a/src/vs/workbench/api/common/apiCommands.ts +++ b/src/vs/workbench/api/common/apiCommands.ts @@ -7,8 +7,6 @@ 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'; -import { IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows'; -import { IWorkspacesService, IRecent } from 'vs/platform/workspaces/common/workspaces'; 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'; @@ -30,100 +28,6 @@ function adjustHandler(handler: (executor: ICommandsExecutor, ...args: any[]) => }; } -interface INewWindowAPICommandOptions { - reuseWindow?: boolean; - /** - * If set, defines the remoteAuthority of the new window. `null` will open a local window. - * If not set, defaults to remoteAuthority of the current window. - */ - remoteAuthority?: string | null; -} - -export class NewWindowAPICommand { - public static readonly ID = 'vscode.newWindow'; - public static execute(executor: ICommandsExecutor, options?: INewWindowAPICommandOptions): Promise { - const commandOptions: IOpenEmptyWindowOptions = { - forceReuseWindow: options && options.reuseWindow, - remoteAuthority: options && options.remoteAuthority - }; - - return executor.executeCommand('_files.newWindow', commandOptions); - } -} -CommandsRegistry.registerCommand({ - id: NewWindowAPICommand.ID, - handler: adjustHandler(NewWindowAPICommand.execute), - description: { - description: 'Opens an new window', - args: [ - ] - } -}); - -CommandsRegistry.registerCommand('_workbench.removeFromRecentlyOpened', function (accessor: ServicesAccessor, uri: URI) { - const workspacesService = accessor.get(IWorkspacesService); - return workspacesService.removeRecentlyOpened([uri]); -}); - -export class RemoveFromRecentlyOpenedAPICommand { - public static readonly ID = 'vscode.removeFromRecentlyOpened'; - public static execute(executor: ICommandsExecutor, path: string | URI): Promise { - if (typeof path === 'string') { - path = path.match(/^[^:/?#]+:\/\//) ? URI.parse(path) : URI.file(path); - } else { - path = URI.revive(path); // called from extension host - } - return executor.executeCommand('_workbench.removeFromRecentlyOpened', path); - } -} -CommandsRegistry.registerCommand(RemoveFromRecentlyOpenedAPICommand.ID, adjustHandler(RemoveFromRecentlyOpenedAPICommand.execute)); - -export interface OpenIssueReporterArgs { - readonly extensionId: string; - readonly issueTitle?: string; - readonly issueBody?: string; -} - -export class OpenIssueReporter { - public static readonly ID = 'vscode.openIssueReporter'; - - public static execute(executor: ICommandsExecutor, args: string | OpenIssueReporterArgs): Promise { - const commandArgs = typeof args === 'string' - ? { extensionId: args } - : args; - return executor.executeCommand('workbench.action.openIssueReporter', commandArgs); - } -} - -interface RecentEntry { - uri: URI; - type: 'workspace' | 'folder' | 'file'; - label?: string; - remoteAuthority?: string; -} - -CommandsRegistry.registerCommand('_workbench.addToRecentlyOpened', async function (accessor: ServicesAccessor, recentEntry: RecentEntry) { - const workspacesService = accessor.get(IWorkspacesService); - let recent: IRecent | undefined = undefined; - const uri = recentEntry.uri; - const label = recentEntry.label; - const remoteAuthority = recentEntry.remoteAuthority; - if (recentEntry.type === 'workspace') { - const workspace = await workspacesService.getWorkspaceIdentifier(uri); - recent = { workspace, label, remoteAuthority }; - } else if (recentEntry.type === 'folder') { - recent = { folderUri: uri, label, remoteAuthority }; - } else { - recent = { fileUri: uri, label, remoteAuthority }; - } - return workspacesService.addRecentlyOpened([recent]); -}); - -CommandsRegistry.registerCommand('_workbench.getRecentlyOpened', async function (accessor: ServicesAccessor) { - const workspacesService = accessor.get(IWorkspacesService); - return workspacesService.getRecentlyOpened(); -}); - CommandsRegistry.registerCommand('_extensionTests.setLogLevel', function (accessor: ServicesAccessor, level: number) { const logService = accessor.get(ILogService); const environmentService = accessor.get(IEnvironmentService); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 93953b68f2..da05bc1ae8 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import * as errors from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; @@ -29,7 +28,7 @@ import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocu import { Extension, IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { ExtHostFileSystem } from 'vs/workbench/api/common/extHostFileSystem'; import { ExtHostFileSystemEventService } from 'vs/workbench/api/common/extHostFileSystemEventService'; -import { ExtHostLanguageFeatures } from 'vs/workbench/api/common/extHostLanguageFeatures'; +import { ExtHostLanguageFeatures, InlineCompletionController } from 'vs/workbench/api/common/extHostLanguageFeatures'; import { ExtHostLanguages } from 'vs/workbench/api/common/extHostLanguages'; import { ExtHostMessageService } from 'vs/workbench/api/common/extHostMessageService'; import { IExtHostOutputService } from 'vs/workbench/api/common/extHostOutput'; @@ -59,7 +58,7 @@ import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IExtHostDecorations } from 'vs/workbench/api/common/extHostDecorations'; import { IExtHostTask } from 'vs/workbench/api/common/extHostTask'; -// import { IExtHostDebugService } from 'vs/workbench/api/common/extHostDebugService'; +// import { IExtHostDebugService } from 'vs/workbench/api/common/extHostDebugService'; {{SQL CARBON EDIT}} import { IExtHostSearch } from 'vs/workbench/api/common/extHostSearch'; import { ILogService } from 'vs/platform/log/common/log'; import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; @@ -82,11 +81,15 @@ import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSyste 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'; +import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; import { ExtHostNotebookKernels } from 'vs/workbench/api/common/extHostNotebookKernels'; -import { RemoteTrustOption } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { TextSearchCompleteMessageType } from 'vs/workbench/services/search/common/searchExtTypes'; +import { ExtHostNotebookRenderers } from 'vs/workbench/api/common/extHostNotebookRenderers'; +import { Schemas } from 'vs/base/common/network'; +import { matchesScheme } from 'vs/platform/opener/common/opener'; +import { ExtHostNotebookEditors } from 'vs/workbench/api/common/extHostNotebookEditors'; +import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNotebookDocuments'; export interface IExtensionApiFactory { (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; @@ -114,6 +117,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostApiDeprecation = accessor.get(IExtHostApiDeprecationService); const extHostWindow = accessor.get(IExtHostWindow); const extHostSecretState = accessor.get(IExtHostSecretState); + const extHostEditorTabs = accessor.get(IExtHostEditorTabs); // register addressable instances rpcProtocol.set(ExtHostContext.ExtHostFileSystemInfo, extHostFileSystemInfo); @@ -126,6 +130,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I rpcProtocol.set(ExtHostContext.ExtHostWindow, extHostWindow); rpcProtocol.set(ExtHostContext.ExtHostSecretState, extHostSecretState); rpcProtocol.set(ExtHostContext.ExtHostTelemetry, extHostTelemetry); + rpcProtocol.set(ExtHostContext.ExtHostEditorTabs, extHostEditorTabs); // automatically create and register addressable instances const extHostDecorations = rpcProtocol.set(ExtHostContext.ExtHostDecorations, accessor.get(IExtHostDecorations)); @@ -138,16 +143,18 @@ 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)); const extHostDocumentSaveParticipant = rpcProtocol.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostLogService, extHostDocuments, rpcProtocol.getProxy(MainContext.MainThreadBulkEdits))); - const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostLogService, extensionStoragePaths)); - const extHostNotebookKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookKernels, new ExtHostNotebookKernels(rpcProtocol, initData, extHostNotebook)); + const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extensionStoragePaths)); + const extHostNotebookDocuments = rpcProtocol.set(ExtHostContext.ExtHostNotebookDocuments, new ExtHostNotebookDocuments(extHostLogService, extHostNotebook)); + const extHostNotebookEditors = rpcProtocol.set(ExtHostContext.ExtHostNotebookEditors, new ExtHostNotebookEditors(extHostLogService, rpcProtocol, extHostNotebook)); + const extHostNotebookKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookKernels, new ExtHostNotebookKernels(rpcProtocol, initData, extHostNotebook, extHostLogService)); + const extHostNotebookRenderers = rpcProtocol.set(ExtHostContext.ExtHostNotebookRenderers, new ExtHostNotebookRenderers(rpcProtocol, extHostNotebook)); const extHostEditors = rpcProtocol.set(ExtHostContext.ExtHostEditors, new ExtHostEditors(rpcProtocol, extHostDocumentsAndEditors)); const extHostTreeViews = rpcProtocol.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(rpcProtocol.getProxy(MainContext.MainThreadTreeViews), extHostCommands, extHostLogService)); - const extHostEditorInsets = rpcProtocol.set(ExtHostContext.ExtHostEditorInsets, new ExtHostEditorInsets(rpcProtocol.getProxy(MainContext.MainThreadEditorInsets), extHostEditors, initData.environment)); + const extHostEditorInsets = rpcProtocol.set(ExtHostContext.ExtHostEditorInsets, new ExtHostEditorInsets(rpcProtocol.getProxy(MainContext.MainThreadEditorInsets), extHostEditors, initData)); const extHostDiagnostics = rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, new ExtHostDiagnostics(rpcProtocol, extHostLogService)); const extHostLanguageFeatures = rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, new ExtHostLanguageFeatures(rpcProtocol, uriTransformer, extHostDocuments, extHostCommands, extHostDiagnostics, extHostLogService, extHostApiDeprecation)); const extHostFileSystem = rpcProtocol.set(ExtHostContext.ExtHostFileSystem, new ExtHostFileSystem(rpcProtocol, extHostLanguageFeatures)); @@ -160,7 +167,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol)); const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol)); const extHostTimeline = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol, extHostCommands)); - const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, initData.environment, extHostWorkspace, extHostLogService, extHostApiDeprecation)); + const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, { remote: initData.remote }, extHostWorkspace, extHostLogService, extHostApiDeprecation)); const extHostWebviewPanels = rpcProtocol.set(ExtHostContext.ExtHostWebviewPanels, new ExtHostWebviewPanels(rpcProtocol, extHostWebviews, extHostWorkspace)); const extHostCustomEditors = rpcProtocol.set(ExtHostContext.ExtHostCustomEditors, new ExtHostCustomEditors(rpcProtocol, extHostDocuments, extensionStoragePaths, extHostWebviews, extHostWebviewPanels)); const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews)); @@ -219,7 +226,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I })(); const authentication: typeof vscode.authentication = { - getSession(providerId: string, scopes: string[], options?: vscode.AuthenticationGetSessionOptions) { + getSession(providerId: string, scopes: readonly string[], options?: vscode.AuthenticationGetSessionOptions) { return extHostAuthentication.getSession(extension, providerId, scopes, options as any); }, get onDidChangeSessions(): Event { @@ -298,7 +305,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get uriScheme() { return initData.environment.appUriScheme; }, get clipboard(): vscode.Clipboard { return extHostClipboard.value; }, get shell() { - return extHostTerminalService.getDefaultShell(false, configProvider); + return extHostTerminalService.getDefaultShell(false); }, get isTelemetryEnabled() { return extHostTelemetry.getTelemetryEnabled(); @@ -316,12 +323,26 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I allowContributedOpeners: options?.allowContributedOpeners, }); }, - asExternalUri(uri: URI) { + async asExternalUri(uri: URI) { if (uri.scheme === initData.environment.appUriScheme) { return extHostUrls.createAppUri(uri); } - return extHostWindow.asExternalUri(uri, { allowTunneling: !!initData.remote.authority }); + const isHttp = matchesScheme(uri, Schemas.http) || matchesScheme(uri, Schemas.https); + + if (!isHttp) { + checkProposedApiEnabled(extension); // https://github.com/microsoft/vscode/issues/124263 + } + + try { + return await extHostWindow.asExternalUri(uri, { allowTunneling: !!initData.remote.authority }); + } catch (err) { + if (isHttp) { + return uri; + } + + throw err; + } }, get remoteName() { return getRemoteName(initData.remote.authority); @@ -482,6 +503,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, ...triggerCharacters: string[]): vscode.Disposable { return extHostLanguageFeatures.registerCompletionItemProvider(extension, checkSelector(selector), provider, triggerCharacters); }, + registerInlineCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.InlineCompletionItemProvider): vscode.Disposable { + checkProposedApiEnabled(extension); + return extHostLanguageFeatures.registerInlineCompletionsProvider(extension, checkSelector(selector), provider); + }, registerDocumentLinkProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentLinkProvider): vscode.Disposable { return extHostLanguageFeatures.registerDocumentLinkProvider(extension, checkSelector(selector), provider); }, @@ -504,9 +529,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostLanguages.tokenAtPosition(doc, pos); }, - registerInlineHintsProvider(selector: vscode.DocumentSelector, provider: vscode.InlineHintsProvider): vscode.Disposable { + registerInlayHintsProvider(selector: vscode.DocumentSelector, provider: vscode.InlayHintsProvider): vscode.Disposable { checkProposedApiEnabled(extension); - return extHostLanguageFeatures.registerInlineHintsProvider(extension, selector, provider); + return extHostLanguageFeatures.registerInlayHintsProvider(extension, selector, provider); } }; @@ -532,7 +557,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostEditors.showTextDocument(document, columnOrOptions, preserveFocus); }, createTextEditorDecorationType(options: vscode.DecorationRenderOptions): vscode.TextEditorDecorationType { - return extHostEditors.createTextEditorDecorationType(options); + return extHostEditors.createTextEditorDecorationType(extension, options); }, onDidChangeActiveTextEditor(listener, thisArg?, disposables?) { return extHostEditors.onDidChangeActiveTextEditor(listener, thisArg, disposables); @@ -599,26 +624,21 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I showSaveDialog(options) { return extHostDialogs.showSaveDialog(options); }, - createStatusBarItem(alignmentOrOptions?: vscode.StatusBarAlignment | vscode.StatusBarItemOptions, priority?: number): vscode.StatusBarItem { - let id: string; - let name: string; + createStatusBarItem(alignmentOrId?: vscode.StatusBarAlignment | string, priorityOrAlignment?: number | vscode.StatusBarAlignment, priorityArg?: number): vscode.StatusBarItem { + let id: string | undefined; let alignment: number | undefined; - let accessibilityInformation: vscode.AccessibilityInformation | undefined = undefined; + let priority: number | undefined; - if (alignmentOrOptions && typeof alignmentOrOptions !== 'number') { - id = alignmentOrOptions.id; - name = alignmentOrOptions.name; - alignment = alignmentOrOptions.alignment; - priority = alignmentOrOptions.priority; - accessibilityInformation = alignmentOrOptions.accessibilityInformation; + if (typeof alignmentOrId === 'string') { + id = alignmentOrId; + alignment = priorityOrAlignment; + priority = priorityArg; } else { - id = extension.identifier.value; - name = nls.localize('extensionLabel', "{0} (Extension)", extension.displayName || extension.name); - alignment = alignmentOrOptions as number; // {{SQL CARBON EDIT}} strict-null-check - priority = priority; + alignment = alignmentOrId as number; // {{SQL CARBON EDIT}} strict-null-check + priority = priorityOrAlignment; } - return extHostStatusBar.createStatusBarEntry(id, name, alignment, priority, accessibilityInformation); + return extHostStatusBar.createStatusBarEntry(extension, id, alignment, priority); }, setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): vscode.Disposable { return extHostStatusBar.setStatusBarMessage(text, timeoutOrThenable); @@ -647,18 +667,18 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I if ('pty' in nameOrOptions) { return extHostTerminalService.createExtensionTerminal(nameOrOptions); } - if (nameOrOptions.message) { - checkProposedApiEnabled(extension); - } - if (nameOrOptions.icon) { + if (nameOrOptions.iconPath) { checkProposedApiEnabled(extension); } return extHostTerminalService.createTerminalFromOptions(nameOrOptions); } return extHostTerminalService.createTerminal(nameOrOptions, shellPath, shellArgs); }, - registerTerminalLinkProvider(handler: vscode.TerminalLinkProvider): vscode.Disposable { - return extHostTerminalService.registerLinkProvider(handler); + registerTerminalLinkProvider(provider: vscode.TerminalLinkProvider): vscode.Disposable { + return extHostTerminalService.registerLinkProvider(provider); + }, + registerTerminalProfileProvider(id: string, provider: vscode.TerminalProfileProvider): vscode.Disposable { + return extHostTerminalService.registerProfileProvider(id, provider); }, registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider): vscode.Disposable { return extHostTreeViews.registerTreeDataProvider(viewId, treeDataProvider, extension); @@ -715,11 +735,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, onDidChangeNotebookEditorSelection(listener, thisArgs?, disposables?) { checkProposedApiEnabled(extension); - return extHostNotebook.onDidChangeNotebookEditorSelection(listener, thisArgs, disposables); + return extHostNotebookEditors.onDidChangeNotebookEditorSelection(listener, thisArgs, disposables); }, onDidChangeNotebookEditorVisibleRanges(listener, thisArgs?, disposables?) { checkProposedApiEnabled(extension); - return extHostNotebook.onDidChangeNotebookEditorVisibleRanges(listener, thisArgs, disposables); + return extHostNotebookEditors.onDidChangeNotebookEditorVisibleRanges(listener, thisArgs, disposables); }, showNotebookDocument(uriOrDocument, options?) { checkProposedApiEnabled(extension); @@ -736,6 +756,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get onDidChangeOpenEditors() { checkProposedApiEnabled(extension); return extHostEditorTabs.onDidChangeTabs; + }, + getInlineCompletionItemController(provider: vscode.InlineCompletionItemProvider): vscode.InlineCompletionController { + checkProposedApiEnabled(extension); + return InlineCompletionController.get(provider); } }; @@ -847,6 +871,34 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I onWillSaveTextDocument: (listener, thisArgs?, disposables?) => { return extHostDocumentSaveParticipant.getOnWillSaveTextDocumentEvent(extension)(listener, thisArgs, disposables); }, + get notebookDocuments(): vscode.NotebookDocument[] { + return extHostNotebook.notebookDocuments.map(d => d.apiNotebook); + }, + async openNotebookDocument(uriOrType?: URI | string, content?: vscode.NotebookData) { + let uri: URI; + if (URI.isUri(uriOrType)) { + uri = uriOrType; + await extHostNotebook.openNotebookDocument(uriOrType); + } else if (typeof uriOrType === 'string') { + uri = URI.revive(await extHostNotebook.createNotebookDocument({ viewType: uriOrType, content })); + } else { + throw new Error('Invalid arguments'); + } + return extHostNotebook.getNotebookDocument(uri).apiNotebook; + }, + get onDidOpenNotebookDocument(): Event { + return extHostNotebook.onDidOpenNotebookDocument; + }, + get onDidCloseNotebookDocument(): Event { + return extHostNotebook.onDidCloseNotebookDocument; + }, + registerNotebookSerializer(viewType: string, serializer: vscode.NotebookSerializer, options?: vscode.NotebookDocumentContentOptions, registration?: vscode.NotebookRegistrationData) { + return extHostNotebook.registerNotebookSerializer(extension, viewType, serializer, options, extension.enableProposedApi ? registration : undefined); + }, + registerNotebookContentProvider: (viewType: string, provider: vscode.NotebookContentProvider, options?: vscode.NotebookDocumentContentOptions, registration?: vscode.NotebookRegistrationData) => { + checkProposedApiEnabled(extension); + return extHostNotebook.registerNotebookContentProvider(extension, viewType, provider, options, extension.enableProposedApi ? registration : undefined); + }, onDidChangeConfiguration: (listener: (_: any) => any, thisArgs?: any, disposables?: extHostTypes.Disposable[]) => { return configProvider.onDidChangeConfiguration(listener, thisArgs, disposables); }, @@ -864,7 +916,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostTask.registerTaskProvider(extension, type, provider); }, registerFileSystemProvider(scheme, provider, options) { - return extHostFileSystem.registerFileSystemProvider(extension.identifier, scheme, provider, options); + return extHostFileSystem.registerFileSystemProvider(extension.identifier, scheme, provider, options, extension.enableProposedApi); }, get fs() { return extHostConsumerFileSystem.value; @@ -1014,12 +1066,12 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I extHostLogService.warn('Debug API is disabled in Azure Data Studio'); return undefined!; }, - addBreakpoints(breakpoints: vscode.Breakpoint[]) { - extHostLogService.warn('Debug API is disabled in Azure Data Studio'); + addBreakpoints(breakpoints: readonly vscode.Breakpoint[]) { + extHostLogService.warn('Debug API is disabled in Azure Data Studio'); // {{SQL CARBON EDIT}} return undefined!; }, - removeBreakpoints(breakpoints: vscode.Breakpoint[]) { - extHostLogService.warn('Debug API is disabled in Azure Data Studio'); + removeBreakpoints(breakpoints: readonly vscode.Breakpoint[]) { + extHostLogService.warn('Debug API is disabled in Azure Data Studio'); // {{SQL CARBON EDIT}} return undefined!; }, asDebugSourceUri(source: vscode.DebugProtocolSource, session?: vscode.DebugSession): vscode.Uri { @@ -1056,52 +1108,34 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }; // namespace: notebook - const notebook: typeof vscode.notebook = { - openNotebookDocument: (uriComponents) => { - checkProposedApiEnabled(extension); - return extHostNotebook.openNotebookDocument(uriComponents); + const notebooks: typeof vscode.notebooks = { + createNotebookController(id: string, notebookType: string, label: string, handler?, rendererScripts?: vscode.NotebookRendererScript[]) { + return extHostNotebookKernels.createNotebookController(extension, id, notebookType, label, handler, extension.enableProposedApi ? rendererScripts : undefined); }, - get onDidOpenNotebookDocument(): Event { - checkProposedApiEnabled(extension); - return extHostNotebook.onDidOpenNotebookDocument; - }, - get onDidCloseNotebookDocument(): Event { - checkProposedApiEnabled(extension); - return extHostNotebook.onDidCloseNotebookDocument; + registerNotebookCellStatusBarItemProvider: (notebookType: string, provider: vscode.NotebookCellStatusBarItemProvider) => { + return extHostNotebook.registerNotebookCellStatusBarItemProvider(extension, notebookType, provider); }, get onDidSaveNotebookDocument(): Event { checkProposedApiEnabled(extension); - return extHostNotebook.onDidSaveNotebookDocument; - }, - get notebookDocuments(): vscode.NotebookDocument[] { - checkProposedApiEnabled(extension); - return extHostNotebook.notebookDocuments.map(d => d.apiNotebook); - }, - registerNotebookSerializer(viewType, serializer, options) { - checkProposedApiEnabled(extension); - return extHostNotebook.registerNotebookSerializer(extension, viewType, serializer, options); - }, - registerNotebookContentProvider: (viewType, provider, options) => { - checkProposedApiEnabled(extension); - return extHostNotebook.registerNotebookContentProvider(extension, viewType, provider, options); - }, - registerNotebookCellStatusBarItemProvider: (selector: vscode.NotebookSelector, provider: vscode.NotebookCellStatusBarItemProvider) => { - checkProposedApiEnabled(extension); - return extHostNotebook.registerNotebookCellStatusBarItemProvider(extension, selector, provider); + return extHostNotebookDocuments.onDidSaveNotebookDocument; }, createNotebookEditorDecorationType(options: vscode.NotebookDecorationRenderOptions): vscode.NotebookEditorDecorationType { checkProposedApiEnabled(extension); - return extHostNotebook.createNotebookEditorDecorationType(options); + return extHostNotebookEditors.createNotebookEditorDecorationType(options); + }, + createRendererMessaging(rendererId) { + checkProposedApiEnabled(extension); + return extHostNotebookRenderers.createRendererMessaging(rendererId); }, onDidChangeNotebookDocumentMetadata(listener, thisArgs?, disposables?) { checkProposedApiEnabled(extension); - return extHostNotebook.onDidChangeNotebookDocumentMetadata(listener, thisArgs, disposables); + return extHostNotebookDocuments.onDidChangeNotebookDocumentMetadata(listener, thisArgs, disposables); }, onDidChangeNotebookCells(listener, thisArgs?, disposables?) { checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeNotebookCells(listener, thisArgs, disposables); }, - onDidChangeCellExecutionState(listener, thisArgs?, disposables?) { + onDidChangeNotebookCellExecutionState(listener, thisArgs?, disposables?) { checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeNotebookCellExecutionState(listener, thisArgs, disposables); }, @@ -1117,14 +1151,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return new ExtHostNotebookConcatDocument(extHostNotebook, extHostDocuments, notebook, selector); }, - createNotebookCellExecutionTask(uri: vscode.Uri, index: number, kernelId: string): vscode.NotebookCellExecutionTask | undefined { - checkProposedApiEnabled(extension); - return extHostNotebook.createNotebookCellExecution(uri, index, kernelId); - }, - createNotebookController(id, viewType, label, executeHandler, preloads) { - checkProposedApiEnabled(extension); - return extHostNotebookKernels.createNotebookController(extension, id, viewType, label, executeHandler, preloads); - } }; return { @@ -1138,7 +1164,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I env, extensions, languages, - notebook, + notebooks, scm, tasks, test, @@ -1191,6 +1217,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I InlineValueText: extHostTypes.InlineValueText, InlineValueVariableLookup: extHostTypes.InlineValueVariableLookup, InlineValueEvaluatableExpression: extHostTypes.InlineValueEvaluatableExpression, + InlineCompletionTriggerKind: extHostTypes.InlineCompletionTriggerKind, EventEmitter: Emitter, ExtensionKind: extHostTypes.ExtensionKind, ExtensionMode: extHostTypes.ExtensionMode, @@ -1199,9 +1226,12 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I FileDecoration: extHostTypes.FileDecoration, FileSystemError: extHostTypes.FileSystemError, FileType: files.FileType, + FilePermission: files.FilePermission, FoldingRange: extHostTypes.FoldingRange, FoldingRangeKind: extHostTypes.FoldingRangeKind, FunctionBreakpoint: extHostTypes.FunctionBreakpoint, + InlineCompletionItem: extHostTypes.InlineSuggestion, + InlineCompletionList: extHostTypes.InlineSuggestions, Hover: extHostTypes.Hover, IndentAction: languageConfiguration.IndentAction, Location: extHostTypes.Location, @@ -1254,10 +1284,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ViewColumn: extHostTypes.ViewColumn, WorkspaceEdit: extHostTypes.WorkspaceEdit, // proposed api types - InlineHint: extHostTypes.InlineHint, - InlineHintKind: extHostTypes.InlineHintKind, + InlayHint: extHostTypes.InlayHint, + InlayHintKind: extHostTypes.InlayHintKind, RemoteAuthorityResolverError: extHostTypes.RemoteAuthorityResolverError, - RemoteTrustOption: RemoteTrustOption, ResolvedAuthority: extHostTypes.ResolvedAuthority, SourceControlInputBoxValidationType: extHostTypes.SourceControlInputBoxValidationType, ExtensionRuntime: extHostTypes.ExtensionRuntime, @@ -1265,16 +1294,16 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I NotebookRange: extHostTypes.NotebookRange, NotebookCellKind: extHostTypes.NotebookCellKind, NotebookCellExecutionState: extHostTypes.NotebookCellExecutionState, - NotebookDocumentMetadata: extHostTypes.NotebookDocumentMetadata, - NotebookCellMetadata: extHostTypes.NotebookCellMetadata, NotebookCellData: extHostTypes.NotebookCellData, NotebookData: extHostTypes.NotebookData, + NotebookRendererScript: extHostTypes.NotebookRendererScript, NotebookCellStatusBarAlignment: extHostTypes.NotebookCellStatusBarAlignment, NotebookEditorRevealType: extHostTypes.NotebookEditorRevealType, NotebookCellOutput: extHostTypes.NotebookCellOutput, NotebookCellOutputItem: extHostTypes.NotebookCellOutputItem, NotebookCellStatusBarItem: extHostTypes.NotebookCellStatusBarItem, NotebookControllerAffinity: extHostTypes.NotebookControllerAffinity, + PortAttributes: extHostTypes.PortAttributes, LinkedEditingRanges: extHostTypes.LinkedEditingRanges, TestItemStatus: extHostTypes.TestItemStatus, TestResultState: extHostTypes.TestResultState, diff --git a/src/vs/workbench/api/common/extHost.common.services.ts b/src/vs/workbench/api/common/extHost.common.services.ts index cf4e92b198..06aaf67b6a 100644 --- a/src/vs/workbench/api/common/extHost.common.services.ts +++ b/src/vs/workbench/api/common/extHost.common.services.ts @@ -12,7 +12,7 @@ import { IExtHostCommands, ExtHostCommands } from 'vs/workbench/api/common/extHo import { IExtHostDocumentsAndEditors, ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostTerminalService, WorkerExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; import { IExtHostTask, WorkerExtHostTask } from 'vs/workbench/api/common/extHostTask'; -// import { IExtHostDebugService, WorkerExtHostDebugService } from 'vs/workbench/api/common/extHostDebugService'; +// import { IExtHostDebugService, WorkerExtHostDebugService } from 'vs/workbench/api/common/extHostDebugService'; {{SQL CARBON EDIT}} import { IExtHostSearch, ExtHostSearch } from 'vs/workbench/api/common/extHostSearch'; import { IExtensionStoragePaths, ExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import { IExtHostStorage, ExtHostStorage } from 'vs/workbench/api/common/extHostStorage'; @@ -23,13 +23,14 @@ import { IExtHostConsumerFileSystem, ExtHostConsumerFileSystem } from 'vs/workbe import { IExtHostFileSystemInfo, ExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; import { IExtHostSecretState, ExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState'; import { ExtHostTelemetry, IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; +import { ExtHostEditorTabs, IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; registerSingleton(IExtensionStoragePaths, ExtensionStoragePaths); registerSingleton(IExtHostApiDeprecationService, ExtHostApiDeprecationService); registerSingleton(IExtHostCommands, ExtHostCommands); registerSingleton(IExtHostConfiguration, ExtHostConfiguration); registerSingleton(IExtHostConsumerFileSystem, ExtHostConsumerFileSystem); -// registerSingleton(IExtHostDebugService, WorkerExtHostDebugService); +// registerSingleton(IExtHostDebugService, WorkerExtHostDebugService); {{SQL CARBON EDIT}} registerSingleton(IExtHostDecorations, ExtHostDecorations); registerSingleton(IExtHostDocumentsAndEditors, ExtHostDocumentsAndEditors); registerSingleton(IExtHostFileSystemInfo, ExtHostFileSystemInfo); @@ -43,3 +44,4 @@ registerSingleton(IExtHostWindow, ExtHostWindow); registerSingleton(IExtHostWorkspace, ExtHostWorkspace); registerSingleton(IExtHostSecretState, ExtHostSecretState); registerSingleton(IExtHostTelemetry, ExtHostTelemetry); +registerSingleton(IExtHostEditorTabs, ExtHostEditorTabs); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 538f174e32..cee8a7f268 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3,7 +3,6 @@ * 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'; @@ -11,7 +10,10 @@ import { SerializedError } from 'vs/base/common/errors'; import { IRelativePattern } from 'vs/base/common/glob'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { revive } from 'vs/base/common/marshalling'; +import * as performance from 'vs/base/common/performance'; import Severity from 'vs/base/common/severity'; +import { Dto } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { RenderLineNumbersType, TextEditorCursorStyle } from 'vs/editor/common/config/editorOptions'; import { IPosition } from 'vs/editor/common/core/position'; @@ -22,8 +24,9 @@ import { EndOfLineSequence, ISingleEditOperation } from 'vs/editor/common/model' import { IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel'; import * as modes from 'vs/editor/common/modes'; import { CharacterPair, CommentRule, EnterAction } from 'vs/editor/common/modes/languageConfiguration'; +import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; -import { ConfigurationTarget, IConfigurationData, IConfigurationChange, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationTarget, IConfigurationChange, IConfigurationData, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import * as files from 'vs/platform/files/common/files'; @@ -32,40 +35,35 @@ import { LogLevel } from 'vs/platform/log/common/log'; import { IMarkerData } from 'vs/platform/markers/common/markers'; import { IProgressOptions, IProgressStep } from 'vs/platform/progress/common/progress'; import * as quickInput from 'vs/platform/quickinput/common/quickInput'; -import { RemoteAuthorityResolverErrorCode, ResolverResult, TunnelDescription, IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import * as statusbar from 'vs/workbench/services/statusbar/common/statusbar'; +import { IRemoteConnectionData, RemoteAuthorityResolverErrorCode, ResolverResult, TunnelDescription } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { ProvidedPortAttributes, TunnelCreationOptions, TunnelOptions, TunnelProviderFeatures } from 'vs/platform/remote/common/tunnel'; import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; import { ITelemetryInfo } from 'vs/platform/telemetry/common/telemetry'; -import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { TreeDataTransferDTO } from 'vs/workbench/api/common/shared/treeDataTransfer'; -import * as tasks from 'vs/workbench/api/common/shared/tasks'; -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 { ActivationKind, MissingExtensionDependency, 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 { IShellLaunchConfig, IShellLaunchConfigDto, ITerminalDimensions, ITerminalEnvironment, ITerminalLaunchError, ITerminalProfile } from 'vs/platform/terminal/common/terminal'; +import { ThemeColor, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/extensionsStorageSync'; +import { WorkspaceTrustRequestOptions } from 'vs/platform/workspace/common/workspaceTrust'; import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; -import { TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions, ProvidedPortAttributes } 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 { NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEventDto, NotebookDataDto, IMainCellDto, TransientCellMetadata, INotebookDecorationRenderOptions, INotebookExclusiveDocumentFilter, IOutputDto, TransientOptions, IImmediateCellEditOperation, INotebookCellStatusBarItem, TransientDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; -import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; -import { Dto } from 'vs/base/common/types'; import { DebugConfigurationProviderTriggerKind, TestResultState } from 'vs/workbench/api/common/extHostTypes'; -import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; -import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/extensionsStorageSync'; -import { InternalTestItem, RunTestForProviderRequest, RunTestsRequest, TestIdWithSrc, TestsDiff, ISerializedTestResults, ITestMessage, ITestItem, ITestRunTask, ExtensionRunTestsRequest } from 'vs/workbench/contrib/testing/common/testCollection'; -import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; -import { WorkspaceTrustRequestOptions } from 'vs/platform/workspace/common/workspaceTrust'; -import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { IShellLaunchConfig, IShellLaunchConfigDto, ITerminalDimensions, ITerminalEnvironment, ITerminalLaunchError } from 'vs/platform/terminal/common/terminal'; -import { ITerminalProfile } from 'vs/workbench/contrib/terminal/common/terminal'; -import { NotebookSelector } from 'vs/workbench/contrib/notebook/common/notebookSelector'; +import * as tasks from 'vs/workbench/api/common/shared/tasks'; +import { EditorGroupColumn, SaveReason } from 'vs/workbench/common/editor'; +import { IRevealOptions, ITreeItem } from 'vs/workbench/common/views'; +import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; +import { IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench/contrib/debug/common/debug'; +import { CellKind, ICellEditOperation, IImmediateCellEditOperation, IMainCellDto, INotebookCellStatusBarItem, INotebookContributionData, INotebookDecorationRenderOptions, IOutputDto, NotebookCellMetadata, NotebookCellsChangedEventDto, NotebookDataDto, NotebookDocumentMetadata, TransientCellMetadata, TransientDocumentMetadata, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { InputValidationType } from 'vs/workbench/contrib/scm/common/scm'; +import { ITextQueryBuilderOptions } from 'vs/workbench/contrib/search/common/queryBuilder'; +import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { ExtensionRunTestsRequest, InternalTestItem, ISerializedTestResults, ITestItem, ITestMessage, ITestRunTask, RunTestForProviderRequest, RunTestsRequest, TestIdWithSrc, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTimelineOptions, Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; +import { ActivationKind, ExtensionHostKind, MissingExtensionDependency } from 'vs/workbench/services/extensions/common/extensions'; +import { createExtHostContextProxyIdentifier as createExtId, createMainContextProxyIdentifier as createMainId, IRPCProtocol } from 'vs/workbench/services/extensions/common/proxyIdentifier'; +import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import * as search from 'vs/workbench/services/search/common/search'; +import * as statusbar from 'vs/workbench/services/statusbar/common/statusbar'; // {{SQL CARBON EDIT}} import { ITreeItem as sqlITreeItem } from 'sql/workbench/common/views'; @@ -80,8 +78,6 @@ export interface IEnvironment { extensionTestsLocationURI?: URI; globalStorageHome: URI; workspaceStorageHome: URI; - webviewResourceRoot: string; - webviewCspSource: string; useHostProxy?: boolean; } @@ -176,7 +172,7 @@ export interface MainThreadAuthenticationShape extends IDisposable { $unregisterAuthenticationProvider(id: string): void; $ensureProvider(id: string): Promise; $sendDidChangeSessions(providerId: string, event: modes.AuthenticationSessionsChangeEvent): void; - $getSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: { createIfNone?: boolean, clearSessionPreference?: boolean }): Promise; + $getSession(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string, options: { createIfNone?: boolean, clearSessionPreference?: boolean }): Promise; $removeSession(providerId: string, sessionId: string): Promise; } @@ -279,7 +275,7 @@ export interface MainThreadBulkEditsShape extends IDisposable { export interface MainThreadTextEditorsShape extends IDisposable { $tryShowTextDocument(resource: UriComponents, options: ITextDocumentShowOptions): Promise; - $registerTextEditorDecorationType(key: string, options: editorCommon.IDecorationRenderOptions): void; + $registerTextEditorDecorationType(extensionId: ExtensionIdentifier, key: string, options: editorCommon.IDecorationRenderOptions): void; $removeTextEditorDecorationType(key: string): void; $tryShowEditor(id: string, position: EditorGroupColumn): Promise; $tryHideEditor(id: string): Promise; @@ -376,6 +372,14 @@ export interface ISignatureHelpProviderMetadataDto { readonly retriggerCharacters: readonly string[]; } +export interface IdentifiableInlineCompletions extends modes.InlineCompletions { + pid: number; +} + +export interface IdentifiableInlineCompletion extends modes.InlineCompletion { + idx: number; +} + export interface MainThreadLanguageFeaturesShape extends IDisposable { $unregister(handle: number): void; $registerDocumentSymbolProvider(handle: number, selector: IDocumentFilterDto[], label: string): void; @@ -402,9 +406,10 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $emitDocumentSemanticTokensEvent(eventHandle: number): void; $registerDocumentRangeSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend): void; $registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void; + $registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[]): void; $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void; - $registerInlineHintsProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void; - $emitInlineHintsEvent(eventHandle: number, event?: any): void; + $registerInlayHintsProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void; + $emitInlayHintsEvent(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; @@ -463,7 +468,7 @@ export interface TerminalLaunchConfig { shellArgs?: string[] | string; cwd?: string | UriComponents; env?: ITerminalEnvironment; - icon?: string; + icon?: URI | { light: URI; dark: URI } | ThemeIcon; initialText?: string; waitOnExit?: boolean; strictEnv?: boolean; @@ -471,6 +476,8 @@ export interface TerminalLaunchConfig { isExtensionCustomPtyTerminal?: boolean; isFeatureTerminal?: boolean; isExtensionOwnedTerminal?: boolean; + useShellEnvironment?: boolean; + isSplitTerminal?: boolean; } export interface MainThreadTerminalServiceShape extends IDisposable { @@ -484,6 +491,8 @@ export interface MainThreadTerminalServiceShape extends IDisposable { $startLinkProvider(): void; $stopLinkProvider(): void; $registerProcessSupport(isSupported: boolean): void; + $registerProfileProvider(id: string): void; + $unregisterProfileProvider(id: string): void; $setEnvironmentVariableCollection(extensionIdentifier: string, persistent: boolean, collection: ISerializableEnvironmentVariableCollection | undefined): void; // Process @@ -628,7 +637,8 @@ export interface MainThreadEditorTabsShape extends IDisposable { export interface IEditorTabDto { group: number; name: string; - resource: UriComponents + resource: UriComponents; + isActive: boolean; } export interface IExtHostEditorTabsShape { @@ -652,7 +662,6 @@ export interface WebviewExtensionDescription { export interface NotebookExtensionDescription { readonly id: ExtensionIdentifier; readonly location: UriComponents; - readonly description?: string; } export enum WebviewEditorCapabilities { @@ -713,7 +722,7 @@ export interface WebviewMessageArrayBufferReference { export interface MainThreadWebviewsShape extends IDisposable { $setHtml(handle: WebviewHandle, value: string): void; $setOptions(handle: WebviewHandle, options: IWebviewOptions): void; - $postMessage(handle: WebviewHandle, value: any, ...buffers: VSBuffer[]): Promise + $postMessage(handle: WebviewHandle, value: string, ...buffers: VSBuffer[]): Promise } export interface MainThreadWebviewPanelsShape extends IDisposable { @@ -823,11 +832,6 @@ export interface ExtHostWebviewViewsShape { $disposeWebviewView(webviewHandle: WebviewHandle): void; } -export enum CellKind { - Markdown = 1, - Code = 2 -} - export enum CellOutputKind { Text = 1, Error = 2, @@ -878,19 +882,14 @@ export interface INotebookCellStatusBarListDto { } export interface MainThreadNotebookShape extends IDisposable { - $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, options: { - transientOutputs: boolean; - transientCellMetadata: TransientCellMetadata; - transientDocumentMetadata: TransientDocumentMetadata; - viewOptions?: { displayName: string; filenamePattern: (string | IRelativePattern | INotebookExclusiveDocumentFilter)[]; exclusive: boolean; }; - }): Promise; + $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, options: TransientOptions, registration: INotebookContributionData | undefined): Promise; $updateNotebookProviderOptions(viewType: string, options?: { transientOutputs: boolean; transientCellMetadata: TransientCellMetadata; transientDocumentMetadata: TransientDocumentMetadata; }): Promise; $unregisterNotebookProvider(viewType: string): Promise; - $registerNotebookSerializer(handle: number, extension: NotebookExtensionDescription, viewType: string, options: TransientOptions): void; + $registerNotebookSerializer(handle: number, extension: NotebookExtensionDescription, viewType: string, options: TransientOptions, registration: INotebookContributionData | undefined): void; $unregisterNotebookSerializer(handle: number): void; - $registerNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined, selector: NotebookSelector): Promise; + $registerNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined, viewType: string): Promise; $unregisterNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined): Promise; $emitCellStatusBarEvent(eventHandle: number): void; } @@ -900,19 +899,21 @@ export interface MainThreadNotebookEditorsShape extends IDisposable { $tryRevealRange(id: string, range: ICellRange, revealType: NotebookEditorRevealType): Promise; $registerNotebookEditorDecorationType(key: string, options: INotebookDecorationRenderOptions): void; $removeNotebookEditorDecorationType(key: string): void; + $trySetSelections(id: string, range: ICellRange[]): void; $trySetDecorations(id: string, range: ICellRange, decorationKey: string): void; $tryApplyEdits(editorId: string, modelVersionId: number, cellEdits: ICellEditOperation[]): Promise } export interface MainThreadNotebookDocumentsShape extends IDisposable { - $tryOpenDocument(uriComponents: UriComponents): Promise; - $trySaveDocument(uri: UriComponents): Promise; + $tryCreateNotebook(options: { viewType: string, content?: NotebookDataDto }): Promise; + $tryOpenNotebook(uriComponents: UriComponents): Promise; + $trySaveNotebook(uri: UriComponents): Promise; $applyEdits(resource: UriComponents, edits: IImmediateCellEditOperation[], computeUndoRedo?: boolean): Promise; } export interface INotebookKernelDto2 { id: string; - viewType: string; + notebookType: string; extensionId: ExtensionIdentifier; extensionLocation: UriComponents; label: string; @@ -920,7 +921,7 @@ export interface INotebookKernelDto2 { description?: string; supportedLanguages?: string[]; supportsInterrupt?: boolean; - hasExecutionOrder?: boolean; + supportsExecutionOrder?: boolean; preloads?: { uri: UriComponents; provides: string[] }[]; } @@ -932,6 +933,10 @@ export interface MainThreadNotebookKernelsShape extends IDisposable { $updateNotebookPriority(handle: number, uri: UriComponents, value: number | undefined): void; } +export interface MainThreadNotebookRenderersShape extends IDisposable { + $postMessage(editorId: string, rendererId: string, message: unknown): void; +} + export interface MainThreadUrlsShape extends IDisposable { $registerUriHandler(handle: number, extensionId: ExtensionIdentifier): Promise; $unregisterUriHandler(handle: number): Promise; @@ -1312,6 +1317,7 @@ export type IResolveAuthorityResult = IResolveAuthorityErrorResult | IResolveAut export interface ExtHostExtensionServiceShape { $resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise; + $getCanonicalURI(remoteAuthority: string, uri: UriComponents): Promise; $startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise; $extensionTestsExecute(): Promise; $extensionTestsExit(code: number): Promise; @@ -1450,17 +1456,16 @@ export interface ISignatureHelpContextDto { readonly activeSignatureHelp?: ISignatureHelpDto; } -export interface IInlineHintDto { +export interface IInlayHintDto { text: string; - range: IRange; - kind: modes.InlineHintKind; + position: IPosition; + kind: modes.InlayHintKind; whitespaceBefore?: boolean; whitespaceAfter?: boolean; - hoverMessage?: string; } -export interface IInlineHintsDto { - hints: IInlineHintDto[] +export interface IInlayHintsDto { + hints: IInlayHintDto[] } export interface ILocationDto { @@ -1653,9 +1658,12 @@ export interface ExtHostLanguageFeaturesShape { $provideCompletionItems(handle: number, resource: UriComponents, position: IPosition, context: modes.CompletionContext, token: CancellationToken): Promise; $resolveCompletionItem(handle: number, id: ChainedCacheId, token: CancellationToken): Promise; $releaseCompletionItems(handle: number, id: number): void; + $provideInlineCompletions(handle: number, resource: UriComponents, position: IPosition, context: modes.InlineCompletionContext, token: CancellationToken): Promise; + $handleInlineCompletionDidShow(handle: number, pid: number, idx: number): void; + $freeInlineCompletionsList(handle: number, pid: 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 + $provideInlayHints(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; @@ -1686,11 +1694,6 @@ export interface ExtHostTelemetryShape { $onDidChangeTelemetryEnabled(enabled: boolean): void; } -export interface IShellAndArgsDto { - shell: string; - args: string[] | string | undefined; -} - export interface ITerminalLinkDto { /** The ID of the link to enable activation and disposal. */ id: number; @@ -1724,11 +1727,11 @@ export interface ExtHostTerminalServiceShape { $acceptProcessRequestInitialCwd(id: number): void; $acceptProcessRequestCwd(id: number): void; $acceptProcessRequestLatency(id: number): number; - $getAvailableProfiles(configuredProfilesOnly: boolean): Promise; - $getDefaultShellAndArgs(useAutomationShell: boolean): Promise; $provideLinks(id: number, line: string): Promise; $activateLink(id: number, linkId: number): void; $initEnvironmentVariableCollections(collections: [string, ISerializableEnvironmentVariableCollection][]): void; + $acceptDefaultProfile(profile: ITerminalProfile, automationProfile: ITerminalProfile): void; + $createContributedProfileTerminal(id: string, isSplitTerminal: boolean): Promise; } export interface ExtHostSCMShape { @@ -1747,7 +1750,6 @@ export interface ExtHostTaskShape { $onDidEndTaskProcess(value: tasks.TaskProcessEndedDTO): void; $OnDidEndTask(execution: tasks.TaskExecutionDTO): void; $resolveVariables(workspaceFolder: UriComponents, toResolve: { process?: { name: string; cwd?: string; }, variables: string[]; }): Promise<{ process?: string; variables: { [key: string]: string; }; }>; - $getDefaultShellAndArgs(): Thenable<{ shell: string, args: string[] | string | undefined; }>; $jsonTasksSupported(): Thenable; $findExecutable(command: string, cwd?: string, paths?: string[]): Promise; } @@ -1806,6 +1808,7 @@ export interface IDebugSessionFullDto { id: DebugSessionUUID; type: string; name: string; + parent: DebugSessionUUID | undefined; folderUri: UriComponents | undefined; configuration: IConfig; } @@ -1913,22 +1916,7 @@ export interface INotebookDocumentsAndEditorsDelta { visibleEditors?: string[]; } -export interface INotebookKernelInfoDto2 { - id?: string; - friendlyId: string; - label: string; - extension: ExtensionIdentifier; - extensionLocation: UriComponents; - providerHandle?: number; - description?: string; - detail?: string; - isPreferred?: boolean; - preloads?: { uri: UriComponents; provides: string[] }[]; - supportedLanguages?: string[] - implementsInterrupt?: boolean; -} - -export interface ExtHostNotebookShape extends ExtHostNotebookDocumentsAndEditorsShape, ExtHostNotebookDocumentsShape, ExtHostNotebookEditorsShape { +export interface ExtHostNotebookShape extends ExtHostNotebookDocumentsAndEditorsShape { $provideNotebookCellStatusBarItems(handle: number, uri: UriComponents, index: number, token: CancellationToken): Promise; $releaseNotebookCellStatusBarItems(id: number): void; @@ -1941,6 +1929,10 @@ export interface ExtHostNotebookShape extends ExtHostNotebookDocumentsAndEditors $notebookToData(handle: number, data: NotebookDataDto, token: CancellationToken): Promise; } +export interface ExtHostNotebookRenderersShape { + $postRendererMessage(editorId: string, rendererId: string, message: unknown): void; +} + export interface ExtHostNotebookDocumentsAndEditorsShape { $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): void; } @@ -1960,10 +1952,10 @@ export interface ExtHostNotebookEditorsShape { } export interface ExtHostNotebookKernelsShape { - $acceptSelection(handle: number, uri: UriComponents, value: boolean): void; + $acceptNotebookAssociation(handle: number, uri: UriComponents, value: boolean): void; $executeCells(handle: number, uri: UriComponents, handles: number[]): Promise; $cancelCells(handle: number, uri: UriComponents, handles: number[]): Promise; - $acceptRendererMessage(handle: number, editorId: string, message: any): void; + $acceptKernelMessageFromRenderer(handle: number, editorId: string, message: any): void; } export interface ExtHostStorageShape { @@ -2094,6 +2086,7 @@ export const MainContext = { MainThreadNotebookDocuments: createMainId('MainThreadNotebookDocumentsShape'), MainThreadNotebookEditors: createMainId('MainThreadNotebookEditorsShape'), MainThreadNotebookKernels: createMainId('MainThreadNotebookKernels'), + MainThreadNotebookRenderers: createMainId('MainThreadNotebookRenderers'), MainThreadTheming: createMainId('MainThreadTheming'), MainThreadTunnelService: createMainId('MainThreadTunnelService'), MainThreadTimeline: createMainId('MainThreadTimeline'), @@ -2140,7 +2133,10 @@ export const ExtHostContext = { ExtHostOutputService: createMainId('ExtHostOutputService'), ExtHosLabelService: createMainId('ExtHostLabelService'), ExtHostNotebook: createMainId('ExtHostNotebook'), + ExtHostNotebookDocuments: createMainId('ExtHostNotebookDocuments'), + ExtHostNotebookEditors: createMainId('ExtHostNotebookEditors'), ExtHostNotebookKernels: createMainId('ExtHostNotebookKernels'), + ExtHostNotebookRenderers: createMainId('ExtHostNotebookRenderers'), ExtHostTheming: createMainId('ExtHostTheming'), ExtHostTunnelService: createMainId('ExtHostTunnelService'), ExtHostAuthentication: createMainId('ExtHostAuthentication'), diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index f78a70db34..833b5779e3 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -4,17 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import type * as vscode from 'vscode'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import * as types from 'vs/workbench/api/common/extHostTypes'; import { IRawColorInfo, IWorkspaceEditDto, ICallHierarchyItemDto, IIncomingCallDto, IOutgoingCallDto } from 'vs/workbench/api/common/extHost.protocol'; import * as modes from 'vs/editor/common/modes'; import * as search from 'vs/workbench/contrib/search/common/search'; -import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; import { ApiCommand, ApiCommandArgument, ApiCommandResult, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { CustomCodeAction } from 'vs/workbench/api/common/extHostLanguageFeatures'; -import { ICommandsExecutor, RemoveFromRecentlyOpenedAPICommand, OpenIssueReporter, OpenIssueReporterArgs } from './apiCommands'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { IRange } from 'vs/editor/common/core/range'; import { IPosition } from 'vs/editor/common/core/position'; @@ -326,10 +323,10 @@ const newCommands: ApiCommand[] = [ ), // --- inline hints new ApiCommand( - 'vscode.executeInlineHintProvider', '_executeInlineHintProvider', 'Execute inline hints provider', + 'vscode.executeInlayHintProvider', '_executeInlayHintProvider', 'Execute inlay 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); + new ApiCommandResult('A promise that resolves to an array of Inlay objects', result => { + return result.map(typeConverters.InlayHint.to); }) ), // --- notebooks @@ -420,63 +417,8 @@ export class ExtHostApiCommands { static register(commands: ExtHostCommands) { newCommands.forEach(commands.registerApiCommand, commands); - return new ExtHostApiCommands(commands).registerCommands(); } - private _commands: ExtHostCommands; - private readonly _disposables = new DisposableStore(); - - private constructor(commands: ExtHostCommands) { - this._commands = commands; - } - - registerCommands() { - - - - - - // ----------------------------------------------------------------- - // The following commands are registered on both sides separately. - // - // We are trying to maintain backwards compatibility for cases where - // API commands are encoded as markdown links, for example. - // ----------------------------------------------------------------- - - type ICommandHandler = (...args: any[]) => any; - const adjustHandler = (handler: (executor: ICommandsExecutor, ...args: any[]) => any): ICommandHandler => { - return (...args: any[]) => { - return handler(this._commands, ...args); - }; - }; - - this._register(RemoveFromRecentlyOpenedAPICommand.ID, adjustHandler(RemoveFromRecentlyOpenedAPICommand.execute), { - description: 'Removes an entry with the given path from the recently opened list.', - args: [ - { name: 'path', description: 'Path to remove from recently opened.', constraint: (value: any) => typeof value === 'string' } - ] - }); - - this._register(OpenIssueReporter.ID, adjustHandler(OpenIssueReporter.execute), { - description: 'Opens the issue reporter with the provided extension id as the selected source', - args: [ - { name: 'extensionId', description: 'extensionId to report an issue on', constraint: (value: unknown) => typeof value === 'string' || (typeof value === 'object' && typeof (value as OpenIssueReporterArgs).extensionId === 'string') } - ] - }); - } - - // --- command impl - - /** - * @deprecated use the ApiCommand instead - */ - private _register(id: string, handler: (...args: any[]) => any, description?: ICommandHandlerDescription): void { - const disposable = this._commands.registerCommand(false, id, handler, this, description); - this._disposables.add(disposable); - } - - - } function tryMapWith(f: (x: T) => R) { diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index 07754e0ea8..856d158c28 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -48,11 +48,11 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { return Object.freeze(this._providers.slice()); } - async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions & { createIfNone: true }): Promise; - async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions = {}): Promise { + async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: readonly string[], options: vscode.AuthenticationGetSessionOptions & { createIfNone: true }): Promise; + async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: readonly string[], options: vscode.AuthenticationGetSessionOptions = {}): Promise { const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); const inFlightRequests = this._inFlightRequests.get(extensionId) || []; - const sortedScopes = scopes.sort().join(' '); + const sortedScopes = [...scopes].sort().join(' '); let inFlightRequest: GetSessionsRequest | undefined = inFlightRequests.find(request => request.scopes === sortedScopes); if (inFlightRequest) { @@ -81,7 +81,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { } } - private async _getSession(requestingExtension: IExtensionDescription, extensionId: string, providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions = {}): Promise { + private async _getSession(requestingExtension: IExtensionDescription, extensionId: string, providerId: string, scopes: readonly string[], options: vscode.AuthenticationGetSessionOptions = {}): Promise { await this._proxy.$ensureProvider(providerId); const extensionName = requestingExtension.displayName || requestingExtension.name; return this._proxy.$getSession(providerId, scopes, extensionId, extensionName, options); diff --git a/src/vs/workbench/api/common/extHostCodeInsets.ts b/src/vs/workbench/api/common/extHostCodeInsets.ts index 29c9c4e6fc..ab16294050 100644 --- a/src/vs/workbench/api/common/extHostCodeInsets.ts +++ b/src/vs/workbench/api/common/extHostCodeInsets.ts @@ -8,10 +8,9 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtHostTextEditor } from 'vs/workbench/api/common/extHostTextEditor'; import { ExtHostEditors } from 'vs/workbench/api/common/extHostTextEditors'; +import { asWebviewUri, webviewGenericCspSource, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; import type * as vscode from 'vscode'; import { ExtHostEditorInsetsShape, MainThreadEditorInsetsShape } from './extHost.protocol'; -import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; -import { generateUuid } from 'vs/base/common/uuid'; export class ExtHostEditorInsets implements ExtHostEditorInsetsShape { @@ -61,16 +60,15 @@ export class ExtHostEditorInsets implements ExtHostEditorInsetsShape { const webview = new class implements vscode.Webview { - private readonly _uuid = generateUuid(); private _html: string = ''; private _options: vscode.WebviewOptions = Object.create(null); asWebviewUri(resource: vscode.Uri): vscode.Uri { - return asWebviewUri(that._initData, this._uuid, resource); + return asWebviewUri(resource, that._initData.remote); } get cspSource(): string { - return that._initData.webviewCspSource; + return webviewGenericCspSource; } set options(value: vscode.WebviewOptions) { diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index 8a2f9c6a36..12595aab01 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -170,12 +170,12 @@ export class ExtHostCommands implements ExtHostCommandsShape { const toArgs = cloneAndChange(args, function (value) { if (value instanceof extHostTypes.Position) { return extHostTypeConverter.Position.from(value); - } - if (value instanceof extHostTypes.Range) { + } else if (value instanceof extHostTypes.Range) { return extHostTypeConverter.Range.from(value); - } - if (value instanceof extHostTypes.Location) { + } else if (value instanceof extHostTypes.Location) { return extHostTypeConverter.location.from(value); + } else if (extHostTypes.NotebookRange.isNotebookRange(value)) { + return extHostTypeConverter.NotebookRange.from(value); } if (!Array.isArray(value)) { return value; diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index e4026b09ec..6b6533ba1f 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -31,6 +31,7 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions' import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { withNullAsUndefined } from 'vs/base/common/types'; import * as process from 'vs/base/common/process'; +import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; export const IExtHostDebugService = createDecorator('IExtHostDebugService'); @@ -47,8 +48,8 @@ export interface IExtHostDebugService extends ExtHostDebugServiceShape { onDidChangeBreakpoints: Event; breakpoints: vscode.Breakpoint[]; - addBreakpoints(breakpoints0: vscode.Breakpoint[]): Promise; - removeBreakpoints(breakpoints0: vscode.Breakpoint[]): Promise; + addBreakpoints(breakpoints0: readonly vscode.Breakpoint[]): Promise; + removeBreakpoints(breakpoints0: readonly vscode.Breakpoint[]): Promise; startDebugging(folder: vscode.WorkspaceFolder | undefined, nameOrConfig: string | vscode.DebugConfiguration, options: vscode.DebugSessionOptions): Promise; stopDebugging(session?: vscode.DebugSession): Promise; registerDebugConfigurationProvider(type: string, provider: vscode.DebugConfigurationProvider, trigger: vscode.DebugConfigurationProviderTriggerKind): vscode.Disposable; @@ -109,6 +110,7 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E @IExtHostExtensionService private _extensionService: IExtHostExtensionService, @IExtHostDocumentsAndEditors private _editorsService: IExtHostDocumentsAndEditors, @IExtHostConfiguration protected _configurationService: IExtHostConfiguration, + @IExtHostEditorTabs protected _editorTabs: IExtHostEditorTabs ) { this._configProviderHandleCounter = 0; this._configProviders = []; @@ -843,7 +845,8 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E let ds = this._debugSessions.get(dto.id); if (!ds) { const folder = await this.getFolder(dto.folderUri); - ds = new ExtHostDebugSession(this._debugServiceProxy, dto.id, dto.type, dto.name, folder, dto.configuration); + const parent = dto.parent ? this._debugSessions.get(dto.parent) : undefined; + ds = new ExtHostDebugSession(this._debugServiceProxy, dto.id, dto.type, dto.name, folder, dto.configuration, parent); this._debugSessions.set(ds.id, ds); this._debugServiceProxy.$sessionCached(ds.id); } @@ -870,7 +873,8 @@ export class ExtHostDebugSession implements vscode.DebugSession { private _type: string, private _name: string, private _workspaceFolder: vscode.WorkspaceFolder | undefined, - private _configuration: vscode.DebugConfiguration) { + private _configuration: vscode.DebugConfiguration, + private _parentSession: vscode.DebugSession | undefined) { } public get id(): string { @@ -884,12 +888,15 @@ export class ExtHostDebugSession implements vscode.DebugSession { public get name(): string { return this._name; } - public set name(name: string) { this._name = name; this._debugServiceProxy.$setDebugSessionName(this._id, name); } + public get parentSession(): vscode.DebugSession | undefined { + return this._parentSession; + } + _acceptNameChanged(name: string) { this._name = name; } @@ -930,7 +937,21 @@ export class ExtHostDebugConsole { export class ExtHostVariableResolverService extends AbstractVariableResolverService { - constructor(folders: vscode.WorkspaceFolder[], editorService: ExtHostDocumentsAndEditors | undefined, configurationService: ExtHostConfigProvider, workspaceService?: IExtHostWorkspace) { + constructor(folders: vscode.WorkspaceFolder[], editorService: ExtHostDocumentsAndEditors | undefined, configurationService: ExtHostConfigProvider, editorTabs: IExtHostEditorTabs, workspaceService?: IExtHostWorkspace) { + function getActiveUri(): URI | undefined { + if (editorService) { + const activeEditor = editorService.activeEditor(); + if (activeEditor) { + return activeEditor.document.uri; + } + const tabs = editorTabs.tabs.filter(tab => tab.isActive); + if (tabs.length > 0) { + return tabs[0].resource; + } + } + return undefined; + } + super({ getFolderUri: (folderName: string): URI | undefined => { const found = folders.filter(f => f.name === folderName); @@ -952,19 +973,17 @@ export class ExtHostVariableResolverService extends AbstractVariableResolverServ return process.env['VSCODE_EXEC_PATH']; }, getFilePath: (): string | undefined => { - if (editorService) { - const activeEditor = editorService.activeEditor(); - if (activeEditor) { - return path.normalize(activeEditor.document.uri.fsPath); - } + const activeUri = getActiveUri(); + if (activeUri) { + return path.normalize(activeUri.fsPath); } return undefined; }, getWorkspaceFolderPathForFile: (): string | undefined => { - if (editorService && workspaceService) { - const activeEditor = editorService.activeEditor(); - if (activeEditor) { - const ws = workspaceService.getWorkspaceFolder(activeEditor.document.uri); + if (workspaceService) { + const activeUri = getActiveUri(); + if (activeUri) { + const ws = workspaceService.getWorkspaceFolder(activeUri); if (ws) { return path.normalize(ws.uri.fsPath); } @@ -1076,12 +1095,13 @@ export class WorkerExtHostDebugService extends ExtHostDebugServiceBase { @IExtHostWorkspace workspaceService: IExtHostWorkspace, @IExtHostExtensionService extensionService: IExtHostExtensionService, @IExtHostDocumentsAndEditors editorsService: IExtHostDocumentsAndEditors, - @IExtHostConfiguration configurationService: IExtHostConfiguration + @IExtHostConfiguration configurationService: IExtHostConfiguration, + @IExtHostEditorTabs editorTabs: IExtHostEditorTabs ) { - super(extHostRpcService, workspaceService, extensionService, editorsService, configurationService); + super(extHostRpcService, workspaceService, extensionService, editorsService, configurationService, editorTabs); } protected createVariableResolver(folders: vscode.WorkspaceFolder[], editorService: ExtHostDocumentsAndEditors, configurationService: ExtHostConfigProvider): AbstractVariableResolverService { - return new ExtHostVariableResolverService(folders, editorService, configurationService); + return new ExtHostVariableResolverService(folders, editorService, configurationService, this._editorTabs); } } diff --git a/src/vs/workbench/api/common/extHostDiagnostics.ts b/src/vs/workbench/api/common/extHostDiagnostics.ts index 1ebc9e024d..bce998e273 100644 --- a/src/vs/workbench/api/common/extHostDiagnostics.ts +++ b/src/vs/workbench/api/common/extHostDiagnostics.ts @@ -217,7 +217,7 @@ export class ExtHostDiagnostics implements ExtHostDiagnosticsShape { private readonly _collections = new Map(); private readonly _onDidChangeDiagnostics = new Emitter(); - static _debouncer(last: (vscode.Uri | string)[] | undefined, current: (vscode.Uri | string)[]): (vscode.Uri | string)[] { + static _debouncer(last: vscode.Uri[] | undefined, current: vscode.Uri[]): vscode.Uri[] { if (!last) { return current; } else { @@ -225,24 +225,12 @@ export class ExtHostDiagnostics implements ExtHostDiagnosticsShape { } } - static _mapper(last: (vscode.Uri | string)[]): { uris: vscode.Uri[] } { - const uris: vscode.Uri[] = []; - const map = new Set(); + static _mapper(last: vscode.Uri[]): { uris: readonly vscode.Uri[] } { + const map = new ResourceMap(); for (const uri of last) { - if (typeof uri === 'string') { - if (!map.has(uri)) { - map.add(uri); - uris.push(URI.parse(uri)); - } - } else { - if (!map.has(uri.toString())) { - map.add(uri.toString()); - uris.push(uri); - } - } + map.set(uri, uri); } - Object.freeze(uris); - return { uris }; + return { uris: Object.freeze(Array.from(map.values())) }; } readonly onDidChangeDiagnostics: Event = Event.map(Event.debounce(this._onDidChangeDiagnostics.event, ExtHostDiagnostics._debouncer, 50), ExtHostDiagnostics._mapper); diff --git a/src/vs/workbench/api/common/extHostEditorTabs.ts b/src/vs/workbench/api/common/extHostEditorTabs.ts index cdb75e8034..3c43555603 100644 --- a/src/vs/workbench/api/common/extHostEditorTabs.ts +++ b/src/vs/workbench/api/common/extHostEditorTabs.ts @@ -7,15 +7,25 @@ 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'; - +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export interface IEditorTab { name: string; group: number; resource: vscode.Uri + isActive: boolean } -export class ExtHostEditorTabs implements IExtHostEditorTabsShape { +export interface IExtHostEditorTabs extends IExtHostEditorTabsShape { + readonly _serviceBrand: undefined; + tabs: readonly IEditorTab[]; + onDidChangeTabs: Event; +} + +export const IExtHostEditorTabs = createDecorator('IExtHostEditorTabs'); + +export class ExtHostEditorTabs implements IExtHostEditorTabs { + readonly _serviceBrand: undefined; private readonly _onDidChangeTabs = new Emitter(); readonly onDidChangeTabs: Event = this._onDidChangeTabs.event; @@ -31,7 +41,8 @@ export class ExtHostEditorTabs implements IExtHostEditorTabsShape { return { name: dto.name, group: dto.group, - resource: URI.revive(dto.resource) + resource: URI.revive(dto.resource), + isActive: dto.isActive }; }); this._onDidChangeTabs.fire(); diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 790c35fc09..582caf222b 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -7,10 +7,10 @@ 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 { asPromise, Barrier, timeout } from 'vs/base/common/async'; import { dispose, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; import { TernarySearchTree } from 'vs/base/common/map'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostExtensionServiceShape, IInitData, MainContext, MainThreadExtensionServiceShape, MainThreadTelemetryShape, MainThreadWorkspaceShape, IResolveAuthorityResult } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostConfiguration, IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; @@ -292,19 +292,19 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme try { if (typeof extension.module.deactivate === 'function') { result = Promise.resolve(extension.module.deactivate()).then(undefined, (err) => { - // TODO: Do something with err if this is not the shutdown case + this._logService.error(err); return Promise.resolve(undefined); }); } } catch (err) { - // TODO: Do something with err if this is not the shutdown case + this._logService.error(err); } // clean up subscriptions try { dispose(extension.subscriptions); } catch (err) { - // TODO: Do something with err if this is not the shutdown case + this._logService.error(err); } return result; @@ -631,7 +631,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme // -- called by main thread - public async $resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise { + private async _activateAndGetResolver(remoteAuthority: string): Promise<{ authorityPrefix: string; resolver: vscode.RemoteAuthorityResolver | undefined; }> { const authorityPlusIndex = remoteAuthority.indexOf('+'); if (authorityPlusIndex === -1) { throw new Error(`Not an authority that can be resolved!`); @@ -641,7 +641,12 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme await this._almostReadyToRunExtensions.wait(); await this._activateByEvent(`onResolveRemoteAuthority:${authorityPrefix}`, false); - const resolver = this._resolvers[authorityPrefix]; + return { authorityPrefix, resolver: this._resolvers[authorityPrefix] }; + } + + public async $resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise { + + const { authorityPrefix, resolver } = await this._activateAndGetResolver(remoteAuthority); if (!resolver) { return { type: 'error', @@ -668,7 +673,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme }; const options: ResolvedOptions = { extensionHostEnv: result.extensionHostEnv, - trust: result.trust + isTrusted: result.isTrusted }; return { @@ -695,6 +700,28 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } } + public async $getCanonicalURI(remoteAuthority: string, uriComponents: UriComponents): Promise { + + const { authorityPrefix, resolver } = await this._activateAndGetResolver(remoteAuthority); + if (!resolver) { + throw new Error(`Cannot get canonical URI because no remote extension is installed to resolve ${authorityPrefix}`); + } + + const uri = URI.revive(uriComponents); + + if (typeof resolver.getCanonicalURI === 'undefined') { + // resolver cannot compute canonical URI + return uri; + } + + const result = await asPromise(() => resolver.getCanonicalURI!(uri)); + if (!result) { + return uri; + } + + return result; + } + public $startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise { this._registry.keepOnly(enabledExtensionIds); return this._startExtensionHost(); diff --git a/src/vs/workbench/api/common/extHostFileSystem.ts b/src/vs/workbench/api/common/extHostFileSystem.ts index dd65b7f486..0e050e8cf1 100644 --- a/src/vs/workbench/api/common/extHostFileSystem.ts +++ b/src/vs/workbench/api/common/extHostFileSystem.ts @@ -115,6 +115,7 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { private readonly _fsProvider = new Map(); private readonly _registeredSchemes = new Set(); private readonly _watches = new Map(); + private readonly _enableProposedApi = new Map(); private _linkProviderRegistration?: IDisposable; private _handlePool: number = 0; @@ -133,7 +134,7 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { } } - registerFileSystemProvider(extension: ExtensionIdentifier, scheme: string, provider: vscode.FileSystemProvider, options: { isCaseSensitive?: boolean, isReadonly?: boolean } = {}) { + registerFileSystemProvider(extension: ExtensionIdentifier, scheme: string, provider: vscode.FileSystemProvider, options: { isCaseSensitive?: boolean, isReadonly?: boolean } = {}, enableProposedApi?: boolean) { if (this._registeredSchemes.has(scheme)) { throw new Error(`a provider for the scheme '${scheme}' is already registered`); @@ -146,6 +147,7 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { this._linkProvider.add(scheme); this._registeredSchemes.add(scheme); this._fsProvider.set(handle, provider); + this._enableProposedApi.set(handle, enableProposedApi ?? false); let capabilities = files.FileSystemProviderCapabilities.FileReadWrite; if (options.isCaseSensitive) { @@ -200,17 +202,22 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { this._linkProvider.delete(scheme); this._registeredSchemes.delete(scheme); this._fsProvider.delete(handle); + this._enableProposedApi.delete(handle); this._proxy.$unregisterProvider(handle); }); } - private static _asIStat(stat: vscode.FileStat): files.IStat { - const { type, ctime, mtime, size } = stat; - return { type, ctime, mtime, size }; + private static _asIStat(stat: vscode.FileStat, enableProposedApi: boolean): files.IStat { + const { type, ctime, mtime, size, permissions } = stat; + if (enableProposedApi) { + return { type, ctime, mtime, size, permissions }; + } else { + return { type, ctime, mtime, size }; + } } $stat(handle: number, resource: UriComponents): Promise { - return Promise.resolve(this._getFsProvider(handle).stat(URI.revive(resource))).then(ExtHostFileSystem._asIStat); + return Promise.resolve(this._getFsProvider(handle).stat(URI.revive(resource))).then(stat => ExtHostFileSystem._asIStat(stat, this._enableProposedApi.get(handle) ?? false)); } $readdir(handle: number, resource: UriComponents): Promise<[string, files.FileType][]> { diff --git a/src/vs/workbench/api/common/extHostLabelService.ts b/src/vs/workbench/api/common/extHostLabelService.ts index cc7fb5775d..78bc3a0d97 100644 --- a/src/vs/workbench/api/common/extHostLabelService.ts +++ b/src/vs/workbench/api/common/extHostLabelService.ts @@ -24,4 +24,4 @@ export class ExtHostLabelService implements ExtHostLabelServiceShape { this._proxy.$unregisterResourceLabelFormatter(handle); }); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index f6d7658bc1..4d782f9990 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -19,7 +19,7 @@ import { regExpLeadsToEndlessLoop, regExpFlags } from 'vs/base/common/strings'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange, Range as EditorRange } from 'vs/editor/common/core/range'; import { isFalsyOrEmpty, isNonEmptyArray, coalesce, asArray } from 'vs/base/common/arrays'; -import { isObject } from 'vs/base/common/types'; +import { isArray, isObject } from 'vs/base/common/types'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -33,6 +33,7 @@ import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostAp import { Cache } from './cache'; import { StopWatch } from 'vs/base/common/stopwatch'; import { CancellationError } from 'vs/base/common/errors'; +import { Emitter } from 'vs/base/common/event'; // --- adapter @@ -1038,6 +1039,99 @@ class SuggestAdapter { } } +class InlineCompletionAdapter { + private readonly _cache = new Cache('InlineCompletionItem'); + private readonly _disposables = new Map(); + + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.InlineCompletionItemProvider, + private readonly _commands: CommandsConverter, + ) { } + + public async provideInlineCompletions(resource: URI, position: IPosition, context: vscode.InlineCompletionContext, token: CancellationToken): Promise { + const doc = this._documents.getDocument(resource); + const pos = typeConvert.Position.to(position); + + const result = await asPromise(() => this._provider.provideInlineCompletionItems(doc, pos, context, token)); + + if (!result) { + // undefined and null are valid results + return undefined; + } + + if (token.isCancellationRequested) { + // cancelled -> return without further ado, esp no caching + // of results as they will leak + return undefined; + } + + const normalizedResult: vscode.InlineCompletionList = isArray(result) ? { items: result } : result; + + const pid = this._cache.add(normalizedResult.items); + let disposableStore: DisposableStore | undefined = undefined; + + return { + pid, + items: normalizedResult.items.map((item, idx) => { + let command: modes.Command | undefined = undefined; + if (item.command) { + if (!disposableStore) { + disposableStore = new DisposableStore(); + this._disposables.set(pid, disposableStore); + } + command = this._commands.toInternal(item.command, disposableStore); + } + + return ({ + text: item.text, + range: item.range ? typeConvert.Range.from(item.range) : undefined, + command, + idx: idx, + }); + }), + }; + } + + public disposeCompletions(pid: number) { + this._cache.delete(pid); + const d = this._disposables.get(pid); + if (d) { + d.clear(); + } + this._disposables.delete(pid); + } + + public handleDidShowCompletionItem(pid: number, idx: number): void { + const completionItem = this._cache.get(pid, idx); + if (completionItem) { + InlineCompletionController.get(this._provider).fireOnDidShowCompletionItem({ + completionItem + }); + } + } +} + +export class InlineCompletionController implements vscode.InlineCompletionController { + private static readonly map = new WeakMap, InlineCompletionController>(); + + public static get(provider: vscode.InlineCompletionItemProvider): InlineCompletionController { + let existing = InlineCompletionController.map.get(provider); + if (!existing) { + existing = new InlineCompletionController(); + InlineCompletionController.map.set(provider, existing); + } + return existing; + } + + private readonly _onDidShowCompletionItemEmitter = new Emitter>(); + public readonly onDidShowCompletionItem: vscode.Event> = this._onDidShowCompletionItemEmitter.event; + + public fireOnDidShowCompletionItem(event: vscode.InlineCompletionItemDidShowEvent): void { + this._onDidShowCompletionItemEmitter.fire(event); + } +} + class SignatureHelpAdapter { private readonly _cache = new Cache('SignatureHelp'); @@ -1082,16 +1176,16 @@ class SignatureHelpAdapter { } } -class InlineHintsAdapter { +class InlayHintsAdapter { constructor( private readonly _documents: ExtHostDocuments, - private readonly _provider: vscode.InlineHintsProvider, + private readonly _provider: vscode.InlayHintsProvider, ) { } - provideInlineHints(resource: URI, range: IRange, token: CancellationToken): Promise { + provideInlayHints(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; + return asPromise(() => this._provider.provideInlayHints(doc, typeConvert.Range.to(range), token)).then(value => { + return value ? { hints: value.map(typeConvert.InlayHint.from) } : undefined; }); } } @@ -1105,50 +1199,62 @@ class LinkProviderAdapter { private readonly _provider: vscode.DocumentLinkProvider ) { } - provideLinks(resource: URI, token: CancellationToken): Promise { + async provideLinks(resource: URI, token: CancellationToken): Promise { const doc = this._documents.getDocument(resource); - return asPromise(() => this._provider.provideDocumentLinks(doc, token)).then(links => { - if (!Array.isArray(links) || links.length === 0) { - // bad result - return undefined; - } + const links = await asPromise(() => this._provider.provideDocumentLinks(doc, token)); + if (!Array.isArray(links) || links.length === 0) { + // bad result + return undefined; + } + if (token.isCancellationRequested) { + // cancelled -> return without further ado, esp no caching + // of results as they will leak + return undefined; + } + if (typeof this._provider.resolveDocumentLink !== 'function') { + // no resolve -> no caching + return { links: links.filter(LinkProviderAdapter._validateLink).map(typeConvert.DocumentLink.from) }; - if (token.isCancellationRequested) { - // cancelled -> return without further ado, esp no caching - // of results as they will leak - return undefined; - } + } else { + // cache links for future resolving + const pid = this._cache.add(links); + const result: extHostProtocol.ILinksListDto = { links: [], id: pid }; + for (let i = 0; i < links.length; i++) { - if (typeof this._provider.resolveDocumentLink !== 'function') { - // no resolve -> no caching - return { links: links.map(typeConvert.DocumentLink.from) }; - - } else { - // cache links for future resolving - const pid = this._cache.add(links); - const result: extHostProtocol.ILinksListDto = { links: [], id: pid }; - for (let i = 0; i < links.length; i++) { - const dto: extHostProtocol.ILinkDto = typeConvert.DocumentLink.from(links[i]); - dto.cacheId = [pid, i]; - result.links.push(dto); + if (!LinkProviderAdapter._validateLink(links[i])) { + continue; } - return result; + + const dto: extHostProtocol.ILinkDto = typeConvert.DocumentLink.from(links[i]); + dto.cacheId = [pid, i]; + result.links.push(dto); } - }); + return result; + } } - resolveLink(id: extHostProtocol.ChainedCacheId, token: CancellationToken): Promise { + private static _validateLink(link: vscode.DocumentLink): boolean { + if (link.target && link.target.path.length > 50_000) { + console.warn('DROPPING link because it is too long'); + return false; + } + return true; + } + + async resolveLink(id: extHostProtocol.ChainedCacheId, token: CancellationToken): Promise { if (typeof this._provider.resolveDocumentLink !== 'function') { - return Promise.resolve(undefined); + return undefined; } const item = this._cache.get(...id); if (!item) { - return Promise.resolve(undefined); + return undefined; } - return asPromise(() => this._provider.resolveDocumentLink!(item, token)).then(value => { - return value && typeConvert.DocumentLink.from(value) || undefined; - }); + const link = await asPromise(() => this._provider.resolveDocumentLink!(item, token)); + if (!link || !LinkProviderAdapter._validateLink(link)) { + return undefined; + } + return typeConvert.DocumentLink.from(link); } releaseLinks(id: number): any { @@ -1355,7 +1461,7 @@ type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | Hov | TypeDefinitionAdapter | ColorProviderAdapter | FoldingProviderAdapter | DeclarationAdapter | SelectionRangeAdapter | CallHierarchyAdapter | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter | EvaluatableExpressionAdapter | InlineValuesAdapter - | LinkedEditingRangeAdapter | InlineHintsAdapter; + | LinkedEditingRangeAdapter | InlayHintsAdapter | InlineCompletionAdapter; class AdapterData { constructor( @@ -1809,6 +1915,28 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF this._withAdapter(handle, SuggestAdapter, adapter => adapter.releaseCompletionItems(id), undefined); } + // --- ghost test + + registerInlineCompletionsProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.InlineCompletionItemProvider): vscode.Disposable { + const handle = this._addNewAdapter(new InlineCompletionAdapter(this._documents, provider, this._commands.converter), extension); + this._proxy.$registerInlineCompletionsSupport(handle, this._transformDocumentSelector(selector)); + return this._createDisposable(handle); + } + + $provideInlineCompletions(handle: number, resource: UriComponents, position: IPosition, context: modes.InlineCompletionContext, token: CancellationToken): Promise { + return this._withAdapter(handle, InlineCompletionAdapter, adapter => adapter.provideInlineCompletions(URI.revive(resource), position, context, token), undefined); + } + + $handleInlineCompletionDidShow(handle: number, pid: number, idx: number): void { + this._withAdapter(handle, InlineCompletionAdapter, async adapter => { + adapter.handleDidShowCompletionItem(pid, idx); + }, undefined); + } + + $freeInlineCompletionsList(handle: number, pid: number): void { + this._withAdapter(handle, InlineCompletionAdapter, async adapter => { adapter.disposeCompletions(pid); }, undefined); + } + // --- parameter hints registerSignatureHelpProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.SignatureHelpProvider, metadataOrTriggerChars: string[] | vscode.SignatureHelpProviderMetadata): vscode.Disposable { @@ -1831,23 +1959,23 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF // --- inline hints - registerInlineHintsProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.InlineHintsProvider): vscode.Disposable { + registerInlayHintsProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.InlayHintsProvider): vscode.Disposable { - const eventHandle = typeof provider.onDidChangeInlineHints === 'function' ? this._nextHandle() : undefined; - const handle = this._addNewAdapter(new InlineHintsAdapter(this._documents, provider), extension); + const eventHandle = typeof provider.onDidChangeInlayHints === 'function' ? this._nextHandle() : undefined; + const handle = this._addNewAdapter(new InlayHintsAdapter(this._documents, provider), extension); - this._proxy.$registerInlineHintsProvider(handle, this._transformDocumentSelector(selector), eventHandle); + this._proxy.$registerInlayHintsProvider(handle, this._transformDocumentSelector(selector), eventHandle); let result = this._createDisposable(handle); if (eventHandle !== undefined) { - const subscription = provider.onDidChangeInlineHints!(_ => this._proxy.$emitInlineHintsEvent(eventHandle)); + const subscription = provider.onDidChangeInlayHints!(_ => this._proxy.$emitInlayHintsEvent(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); + $provideInlayHints(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise { + return this._withAdapter(handle, InlayHintsAdapter, adapter => adapter.provideInlayHints(URI.revive(resource), range, token), undefined); } // --- links diff --git a/src/vs/workbench/api/common/extHostMemento.ts b/src/vs/workbench/api/common/extHostMemento.ts index b4b78bab56..92801033fb 100644 --- a/src/vs/workbench/api/common/extHostMemento.ts +++ b/src/vs/workbench/api/common/extHostMemento.ts @@ -56,6 +56,11 @@ export class ExtensionMemento implements vscode.Memento { }, 0); } + get keys(): readonly string[] { + // Filter out `undefined` values, as they can stick around in the `_value` until the `onDidChangeStorage` event runs + return Object.entries(this._value ?? {}).filter(([, value]) => value !== undefined).map(([key]) => key); + } + get whenReady(): Promise { return this._init; } diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 0eb73183e4..b4b0ddfa24 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -3,54 +3,31 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { timeout } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { IRelativePattern } from 'vs/base/common/glob'; import { hash } from 'vs/base/common/hash'; -import { IdGenerator } from 'vs/base/common/idGenerator'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { assertIsDefined } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { ILogService } from 'vs/platform/log/common/log'; import { Cache } from 'vs/workbench/api/common/cache'; -import { ExtHostNotebookShape, IMainContext, IModelAddedData, INotebookCellStatusBarListDto, INotebookDocumentPropertiesChangeData, INotebookDocumentsAndEditorsDelta, INotebookDocumentShowOptions, INotebookEditorAddData, INotebookEditorPropertiesChangeData, INotebookEditorViewColumnInfo, MainContext, MainThreadNotebookDocumentsShape, MainThreadNotebookEditorsShape, MainThreadNotebookShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostNotebookShape, IMainContext, IModelAddedData, INotebookCellStatusBarListDto, INotebookDocumentsAndEditorsDelta, INotebookDocumentShowOptions, INotebookEditorAddData, MainContext, MainThreadNotebookDocumentsShape, MainThreadNotebookEditorsShape, MainThreadNotebookShape } from 'vs/workbench/api/common/extHost.protocol'; import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; -import { CellEditType, IImmediateCellEditOperation, INotebookExclusiveDocumentFilter, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookDataDto, NullablePartialNotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookExclusiveDocumentFilter, INotebookContributionData, NotebookCellsChangeType, NotebookDataDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import type * as vscode from 'vscode'; import { ExtHostCell, ExtHostNotebookDocument } from './extHostNotebookDocument'; import { ExtHostNotebookEditor } from './extHostNotebookEditor'; -export class NotebookEditorDecorationType { - - private static readonly _Keys = new IdGenerator('NotebookEditorDecorationType'); - - readonly value: vscode.NotebookEditorDecorationType; - - constructor(proxy: MainThreadNotebookEditorsShape, options: vscode.NotebookDecorationRenderOptions) { - const key = NotebookEditorDecorationType._Keys.nextId(); - proxy.$registerNotebookEditorDecorationType(key, typeConverters.NotebookDecorationRenderOptions.from(options)); - - this.value = { - key, - dispose() { - proxy.$removeNotebookEditorDecorationType(key); - } - }; - } -} - - type NotebookContentProviderData = { readonly provider: vscode.NotebookContentProvider; readonly extension: IExtensionDescription; @@ -68,12 +45,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { private readonly _documents = new ResourceMap(); private readonly _editors = new Map(); private readonly _commandsConverter: CommandsConverter; - private readonly _onDidChangeNotebookEditorSelection = new Emitter(); - readonly onDidChangeNotebookEditorSelection = this._onDidChangeNotebookEditorSelection.event; - private readonly _onDidChangeNotebookEditorVisibleRanges = new Emitter(); - readonly onDidChangeNotebookEditorVisibleRanges = this._onDidChangeNotebookEditorVisibleRanges.event; - private readonly _onDidChangeNotebookDocumentMetadata = new Emitter(); - readonly onDidChangeNotebookDocumentMetadata = this._onDidChangeNotebookDocumentMetadata.event; + private readonly _onDidChangeNotebookCells = new Emitter(); readonly onDidChangeNotebookCells = this._onDidChangeNotebookCells.event; private readonly _onDidChangeCellOutputs = new Emitter(); @@ -98,13 +70,10 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { onDidOpenNotebookDocument: Event = this._onDidOpenNotebookDocument.event; private _onDidCloseNotebookDocument = new Emitter(); onDidCloseNotebookDocument: Event = this._onDidCloseNotebookDocument.event; - private _onDidSaveNotebookDocument = new Emitter(); - onDidSaveNotebookDocument: Event = this._onDidSaveNotebookDocument.event; + private _onDidChangeVisibleNotebookEditors = new Emitter(); onDidChangeVisibleNotebookEditors = this._onDidChangeVisibleNotebookEditors.event; - private _activeExecutions = new ResourceMap(); - private _statusBarCache = new Cache('NotebookCellStatusBarCache'); constructor( @@ -112,7 +81,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { commands: ExtHostCommands, private _textDocumentsAndEditors: ExtHostDocumentsAndEditors, private _textDocuments: ExtHostDocuments, - private readonly logService: ILogService, private readonly _extensionStoragePaths: IExtensionStoragePaths, ) { this._notebookProxy = mainContext.getProxy(MainContext.MainThreadNotebook); @@ -138,8 +106,12 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { }); } - getEditorById(editorId: string): ExtHostNotebookEditor | undefined { - return this._editors.get(editorId); + getEditorById(editorId: string): ExtHostNotebookEditor { + const editor = this._editors.get(editorId); + if (!editor) { + throw new Error(`unknown text editor: ${editorId}. known editors: ${[...this._editors.keys()]} `); + } + return editor; } getIdByEditor(editor: vscode.NotebookEditor): string | undefined { @@ -155,13 +127,11 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { return [...this._documents.values()]; } - lookupNotebookDocument(uri: URI): ExtHostNotebookDocument | undefined { - return this._documents.get(uri); - } - - private _getNotebookDocument(uri: URI): ExtHostNotebookDocument { + getNotebookDocument(uri: URI, relaxed: true): ExtHostNotebookDocument | undefined; + getNotebookDocument(uri: URI): ExtHostNotebookDocument; + getNotebookDocument(uri: URI, relaxed?: true): ExtHostNotebookDocument | undefined { const result = this._documents.get(uri); - if (!result) { + if (!result && !relaxed) { throw new Error(`NO notebook document for '${uri}'`); } return result; @@ -179,13 +149,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { extension: IExtensionDescription, viewType: string, provider: vscode.NotebookContentProvider, - options?: vscode.NotebookDocumentContentOptions & { - viewOptions?: { - displayName: string; - filenamePattern: (vscode.GlobPattern | { include: vscode.GlobPattern; exclude: vscode.GlobPattern })[]; - exclusive?: boolean; - }; - } + options?: vscode.NotebookDocumentContentOptions, + registration?: vscode.NotebookRegistrationData ): vscode.Disposable { if (isFalsyOrWhitespace(viewType)) { throw new Error(`viewType cannot be empty or just whitespace`); @@ -196,7 +161,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { this._notebookContentProviders.set(viewType, { extension, provider }); - let listener: IDisposable | undefined; if (provider.onDidChangeNotebookContentOptions) { listener = provider.onDidChangeNotebookContentOptions(() => { @@ -205,21 +169,12 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { }); } - const viewOptionsFilenamePattern = options?.viewOptions?.filenamePattern - .map(pattern => typeConverters.NotebookExclusiveDocumentPattern.from(pattern)) - .filter(pattern => pattern !== undefined) as (string | IRelativePattern | INotebookExclusiveDocumentFilter)[]; - - if (options?.viewOptions?.filenamePattern && !viewOptionsFilenamePattern) { - console.warn(`Notebook content provider view options file name pattern is invalid ${options?.viewOptions?.filenamePattern}`); - } - - const internalOptions = typeConverters.NotebookDocumentContentOptions.from(options); - this._notebookProxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, viewType, { - transientOutputs: internalOptions.transientOutputs, - transientCellMetadata: internalOptions.transientCellMetadata, - transientDocumentMetadata: internalOptions.transientDocumentMetadata, - viewOptions: options?.viewOptions && viewOptionsFilenamePattern ? { displayName: options.viewOptions.displayName, filenamePattern: viewOptionsFilenamePattern, exclusive: options.viewOptions.exclusive || false } : undefined - }); + this._notebookProxy.$registerNotebookProvider( + { id: extension.identifier, location: extension.extensionLocation }, + viewType, + typeConverters.NotebookDocumentContentOptions.from(options), + ExtHostNotebookController._convertNotebookRegistrationData(extension, registration) + ); return new extHostTypes.Disposable(() => { listener?.dispose(); @@ -228,13 +183,33 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { }); } - registerNotebookCellStatusBarItemProvider(extension: IExtensionDescription, selector: vscode.NotebookSelector, provider: vscode.NotebookCellStatusBarItemProvider) { + private static _convertNotebookRegistrationData(extension: IExtensionDescription, registration: vscode.NotebookRegistrationData | undefined): INotebookContributionData | undefined { + if (!registration) { + return undefined; // {{SQL CARBON EDIT}} Strict nulls + } + const viewOptionsFilenamePattern = registration.filenamePattern + .map(pattern => typeConverters.NotebookExclusiveDocumentPattern.from(pattern)) + .filter(pattern => pattern !== undefined) as (string | IRelativePattern | INotebookExclusiveDocumentFilter)[]; + if (registration.filenamePattern && !viewOptionsFilenamePattern) { + console.warn(`Notebook content provider view options file name pattern is invalid ${registration.filenamePattern}`); + return undefined; + } + return { + extension: extension.identifier, + providerDisplayName: extension.displayName || extension.name, + displayName: registration.displayName, + filenamePattern: viewOptionsFilenamePattern, + exclusive: registration.exclusive || false + }; + } + + registerNotebookCellStatusBarItemProvider(extension: IExtensionDescription, notebookType: string, provider: vscode.NotebookCellStatusBarItemProvider) { const handle = ExtHostNotebookController._notebookStatusBarItemProviderHandlePool++; const eventHandle = typeof provider.onDidChangeCellStatusBarItems === 'function' ? ExtHostNotebookController._notebookStatusBarItemProviderHandlePool++ : undefined; this._notebookStatusBarItemProviders.set(handle, provider); - this._notebookProxy.$registerNotebookCellStatusBarItemProvider(handle, eventHandle, selector); + this._notebookProxy.$registerNotebookCellStatusBarItemProvider(handle, eventHandle, notebookType); let subscription: vscode.Disposable | undefined; if (eventHandle !== undefined) { @@ -250,8 +225,12 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { }); } - createNotebookEditorDecorationType(options: vscode.NotebookDecorationRenderOptions): vscode.NotebookEditorDecorationType { - return new NotebookEditorDecorationType(this._notebookEditorsProxy, options).value; + async createNotebookDocument(options: { viewType: string, content?: vscode.NotebookData }): Promise { + const canonicalUri = await this._notebookDocumentsProxy.$tryCreateNotebook({ + viewType: options.viewType, + content: options.content && typeConverters.NotebookData.from(options.content) + }); + return URI.revive(canonicalUri); } async openNotebookDocument(uri: URI): Promise { @@ -259,7 +238,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { if (cached) { return cached.apiNotebook; } - const canonicalUri = await this._notebookDocumentsProxy.$tryOpenDocument(uri); + const canonicalUri = await this._notebookDocumentsProxy.$tryOpenNotebook(uri); const document = this._documents.get(URI.revive(canonicalUri)); return assertIsDefined(document?.apiNotebook); } @@ -285,7 +264,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { }; } - const editorId = await this._notebookEditorsProxy.$tryShowNotebookDocument(notebookOrUri.uri, notebookOrUri.viewType, resolvedOptions); + const editorId = await this._notebookEditorsProxy.$tryShowNotebookDocument(notebookOrUri.uri, notebookOrUri.notebookType, resolvedOptions); const editor = editorId && this._editors.get(editorId)?.apiEditor; if (editor) { @@ -293,9 +272,9 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { } if (editorId) { - throw new Error(`Could NOT open editor for "${notebookOrUri.toString()}" because another editor opened in the meantime.`); + throw new Error(`Could NOT open editor for "${notebookOrUri.uri.toString()}" because another editor opened in the meantime.`); } else { - throw new Error(`Could NOT open editor for "${notebookOrUri.toString()}".`); + throw new Error(`Could NOT open editor for "${notebookOrUri.uri.toString()}".`); } } @@ -319,7 +298,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { const disposables = new DisposableStore(); const cacheId = this._statusBarCache.add([disposables]); - const items = (result && result.map(item => typeConverters.NotebookStatusBarItem.from(item, this._commandsConverter, disposables))) ?? undefined; + const resultArr = Array.isArray(result) ? result : [result]; + const items = resultArr.map(item => typeConverters.NotebookStatusBarItem.from(item, this._commandsConverter, disposables)); return { cacheId, items @@ -335,18 +315,18 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { private _handlePool = 0; private readonly _notebookSerializer = new Map(); - registerNotebookSerializer(extension: IExtensionDescription, viewType: string, serializer: vscode.NotebookSerializer, options?: vscode.NotebookDocumentContentOptions): vscode.Disposable { + registerNotebookSerializer(extension: IExtensionDescription, viewType: string, serializer: vscode.NotebookSerializer, options?: vscode.NotebookDocumentContentOptions, registration?: vscode.NotebookRegistrationData): vscode.Disposable { if (isFalsyOrWhitespace(viewType)) { throw new Error(`viewType cannot be empty or just whitespace`); } const handle = this._handlePool++; this._notebookSerializer.set(handle, serializer); - const internalOptions = typeConverters.NotebookDocumentContentOptions.from(options); this._notebookProxy.$registerNotebookSerializer( handle, - { id: extension.identifier, location: extension.extensionLocation, description: extension.description }, + { id: extension.identifier, location: extension.extensionLocation }, viewType, - internalOptions + typeConverters.NotebookDocumentContentOptions.from(options), + ExtHostNotebookController._convertNotebookRegistrationData(extension, registration) ); return toDisposable(() => { this._notebookProxy.$unregisterNotebookSerializer(handle); @@ -359,10 +339,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { throw new Error('NO serializer found'); } const data = await serializer.deserializeNotebook(bytes.buffer, token); - return { - metadata: typeConverters.NotebookDocumentMetadata.from(data.metadata), - cells: data.cells.map(typeConverters.NotebookCellData.from), - }; + return typeConverters.NotebookData.from(data); } async $notebookToData(handle: number, data: NotebookDataDto, token: CancellationToken): Promise { @@ -370,38 +347,30 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { if (!serializer) { throw new Error('NO serializer found'); } - const bytes = await serializer.serializeNotebook({ - metadata: typeConverters.NotebookDocumentMetadata.to(data.metadata), - cells: data.cells.map(typeConverters.NotebookCellData.to) - }, token); + const bytes = await serializer.serializeNotebook(typeConverters.NotebookData.to(data), token); return VSBuffer.wrap(bytes); } - cancelOneNotebookCellExecution(cell: ExtHostCell): void { - const execution = this._activeExecutions.get(cell.uri); - execution?.cancel(); - } - // --- open, save, saveAs, backup async $openNotebook(viewType: string, uri: UriComponents, backupId: string | undefined, untitledDocumentData: VSBuffer | undefined, token: CancellationToken): Promise { const { provider } = this._getProviderData(viewType); const data = await provider.openNotebook(URI.revive(uri), { backupId, untitledDocumentData: untitledDocumentData?.buffer }, token); return { - metadata: typeConverters.NotebookDocumentMetadata.from(data.metadata), + metadata: data.metadata ?? Object.create(null), cells: data.cells.map(typeConverters.NotebookCellData.from), }; } async $saveNotebook(viewType: string, uri: UriComponents, token: CancellationToken): Promise { - const document = this._getNotebookDocument(URI.revive(uri)); + const document = this.getNotebookDocument(URI.revive(uri)); const { provider } = this._getProviderData(viewType); await provider.saveNotebook(document.apiNotebook, token); return true; } async $saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise { - const document = this._getNotebookDocument(URI.revive(uri)); + const document = this.getNotebookDocument(URI.revive(uri)); const { provider } = this._getProviderData(viewType); await provider.saveNotebookAs(URI.revive(target), document.apiNotebook, token); return true; @@ -410,7 +379,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { private _backupIdPool: number = 0; async $backupNotebook(viewType: string, uri: UriComponents, cancellation: CancellationToken): Promise { - const document = this._getNotebookDocument(URI.revive(uri)); + const document = this.getNotebookDocument(URI.revive(uri)); const provider = this._getProviderData(viewType); const storagePath = this._extensionStoragePaths.workspaceValue(provider.extension) ?? this._extensionStoragePaths.globalValue(provider.extension); @@ -422,70 +391,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { return backup.id; } - $acceptModelChanged(uri: UriComponents, event: NotebookCellsChangedEventDto, isDirty: boolean): void { - const document = this._getNotebookDocument(URI.revive(uri)); - document.acceptModelChanged(event, isDirty); - } - - $acceptDirtyStateChanged(uri: UriComponents, isDirty: boolean): void { - const document = this._getNotebookDocument(URI.revive(uri)); - document.acceptModelChanged({ rawEvents: [], versionId: document.apiNotebook.version }, isDirty); - } - - $acceptModelSaved(uri: UriComponents): void { - const document = this._getNotebookDocument(URI.revive(uri)); - this._onDidSaveNotebookDocument.fire(document.apiNotebook); - } - - $acceptEditorPropertiesChanged(id: string, data: INotebookEditorPropertiesChangeData): void { - this.logService.debug('ExtHostNotebook#$acceptEditorPropertiesChanged', id, data); - - const editor = this._editors.get(id); - if (!editor) { - throw new Error(`unknown text editor: ${id}. known editors: ${[...this._editors.keys()]} `); - } - - // ONE: make all state updates - if (data.visibleRanges) { - editor._acceptVisibleRanges(data.visibleRanges.ranges.map(typeConverters.NotebookRange.to)); - } - if (data.selections) { - editor._acceptSelections(data.selections.selections.map(typeConverters.NotebookRange.to)); - } - - // TWO: send all events after states have been updated - if (data.visibleRanges) { - this._onDidChangeNotebookEditorVisibleRanges.fire({ - notebookEditor: editor.apiEditor, - visibleRanges: editor.apiEditor.visibleRanges - }); - } - if (data.selections) { - this._onDidChangeNotebookEditorSelection.fire(Object.freeze({ - notebookEditor: editor.apiEditor, - selections: editor.apiEditor.selections - })); - } - } - - $acceptEditorViewColumns(data: INotebookEditorViewColumnInfo): void { - for (const id in data) { - const editor = this._editors.get(id); - if (!editor) { - throw new Error(`unknown text editor: ${id}. known editors: ${[...this._editors.keys()]} `); - } - editor._acceptViewColumn(typeConverters.ViewColumn.to(data[id])); - } - } - - $acceptDocumentPropertiesChanged(uri: UriComponents, data: INotebookDocumentPropertiesChangeData): void { - this.logService.debug('ExtHostNotebook#$acceptDocumentPropertiesChanged', uri.path, data); - const document = this._getNotebookDocument(URI.revive(uri)); - document.acceptDocumentPropertiesChanged(data); - if (data.metadata) { - this._onDidChangeNotebookDocumentMetadata.fire({ document: document.apiNotebook }); - } - } private _createExtHostEditor(document: ExtHostNotebookDocument, editorId: string, data: INotebookEditorAddData) { @@ -559,7 +464,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { } }, viewType, - modelData.metadata ? typeConverters.NotebookDocumentMetadata.to(modelData.metadata) : new extHostTypes.NotebookDocumentMetadata(), + modelData.metadata ?? Object.create({}), uri, ); @@ -639,213 +544,4 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { this._onDidChangeActiveNotebookEditor.fire(this._activeNotebookEditor?.apiEditor); } } - createNotebookCellExecution(docUri: vscode.Uri, index: number, kernelId: string): vscode.NotebookCellExecutionTask | undefined { - const document = this.lookupNotebookDocument(docUri); - if (!document) { - throw new Error(`Invalid uri: ${docUri} `); - } - - const cell = document.getCellFromIndex(index); - if (!cell) { - throw new Error(`Invalid cell index: ${docUri}, ${index} `); - } - - // TODO@roblou also validate kernelId, once kernel has moved from editor to document - if (this._activeExecutions.has(cell.uri)) { - throw new Error(`duplicate execution for ${cell.uri}`); - } - - const execution = new NotebookCellExecutionTask(docUri, document, cell, this._notebookDocumentsProxy); - this._activeExecutions.set(cell.uri, execution); - const listener = execution.onDidChangeState(() => { - if (execution.state === NotebookCellExecutionTaskState.Resolved) { - execution.dispose(); - listener.dispose(); - this._activeExecutions.delete(cell.uri); - } - }); - - return execution.asApiObject(); - } -} - -enum NotebookCellExecutionTaskState { - Init, - Started, - Resolved -} - -class NotebookCellExecutionTask extends Disposable { - private _onDidChangeState = new Emitter(); - readonly onDidChangeState = this._onDidChangeState.event; - - private _state = NotebookCellExecutionTaskState.Init; - get state(): NotebookCellExecutionTaskState { return this._state; } - - private readonly _tokenSource = this._register(new CancellationTokenSource()); - - private readonly _collector: TimeoutBasedCollector; - - private _executionOrder: number | undefined; - - constructor( - private readonly _uri: vscode.Uri, - private readonly _document: ExtHostNotebookDocument, - private readonly _cell: ExtHostCell, - private readonly _proxy: MainThreadNotebookDocumentsShape) { - super(); - - this._collector = new TimeoutBasedCollector(10, edits => this.applyEdits(edits)); - - this._executionOrder = _cell.internalMetadata.executionOrder; - this.mixinMetadata({ - runState: extHostTypes.NotebookCellExecutionState.Pending, - executionOrder: null - }); - } - - cancel(): void { - this._tokenSource.cancel(); - } - - private async applyEditSoon(edit: IImmediateCellEditOperation): Promise { - await this._collector.addItem(edit); - } - - private async applyEdits(edits: IImmediateCellEditOperation[]): Promise { - return this._proxy.$applyEdits(this._uri, edits, false); - } - - private verifyStateForOutput() { - if (this._state === NotebookCellExecutionTaskState.Init) { - throw new Error('Must call start before modifying cell output'); - } - - if (this._state === NotebookCellExecutionTaskState.Resolved) { - throw new Error('Cannot modify cell output after calling resolve'); - } - } - - private mixinMetadata(mixinMetadata: NullablePartialNotebookCellMetadata) { - const edit: IImmediateCellEditOperation = { editType: CellEditType.PartialMetadata, handle: this._cell.handle, metadata: mixinMetadata }; - this.applyEdits([edit]); - } - - private cellIndexToHandle(cellIndex: number | undefined): number | undefined { - const cell = typeof cellIndex === 'number' ? this._document.getCellFromIndex(cellIndex) : this._cell; - if (!cell) { - return undefined; // {{SQL CARBON EDIT}} Strict null - } - - return cell.handle; - } - - asApiObject(): vscode.NotebookCellExecutionTask { - const that = this; - return Object.freeze({ - get document() { return that._document.apiNotebook; }, - get cell() { return that._cell.apiCell; }, - - get executionOrder() { return that._executionOrder; }, - set executionOrder(v: number | undefined) { - that._executionOrder = v; - that.mixinMetadata({ - executionOrder: v - }); - }, - - start(context?: vscode.NotebookCellExecuteStartContext): void { - if (that._state === NotebookCellExecutionTaskState.Resolved || that._state === NotebookCellExecutionTaskState.Started) { - throw new Error('Cannot call start again'); - } - - that._state = NotebookCellExecutionTaskState.Started; - that._onDidChangeState.fire(); - - that.mixinMetadata({ - runState: extHostTypes.NotebookCellExecutionState.Executing, - runStartTime: context?.startTime ?? null - }); - }, - - end(result?: vscode.NotebookCellExecuteEndContext): void { - if (that._state === NotebookCellExecutionTaskState.Resolved) { - throw new Error('Cannot call resolve twice'); - } - - that._state = NotebookCellExecutionTaskState.Resolved; - that._onDidChangeState.fire(); - - that.mixinMetadata({ - runState: extHostTypes.NotebookCellExecutionState.Idle, - lastRunSuccess: result?.success ?? null, - runEndTime: result?.endTime ?? null, - }); - }, - - clearOutput(cellIndex?: number): Thenable { - that.verifyStateForOutput(); - return this.replaceOutput([], cellIndex); - }, - - async appendOutput(outputs: vscode.NotebookCellOutput | vscode.NotebookCellOutput[], cellIndex?: number): Promise { - that.verifyStateForOutput(); - const handle = that.cellIndexToHandle(cellIndex); - if (typeof handle !== 'number') { - return; - } - - outputs = Array.isArray(outputs) ? outputs : [outputs]; - return that.applyEditSoon({ editType: CellEditType.Output, handle, append: true, outputs: outputs.map(typeConverters.NotebookCellOutput.from) }); - }, - - async replaceOutput(outputs: vscode.NotebookCellOutput | vscode.NotebookCellOutput[], cellIndex?: number): Promise { - that.verifyStateForOutput(); - const handle = that.cellIndexToHandle(cellIndex); - if (typeof handle !== 'number') { - return; - } - - outputs = Array.isArray(outputs) ? outputs : [outputs]; - return that.applyEditSoon({ editType: CellEditType.Output, handle, outputs: outputs.map(typeConverters.NotebookCellOutput.from) }); - }, - - async appendOutputItems(items: vscode.NotebookCellOutputItem | vscode.NotebookCellOutputItem[], outputId: string): Promise { - that.verifyStateForOutput(); - items = Array.isArray(items) ? items : [items]; - return that.applyEditSoon({ editType: CellEditType.OutputItems, append: true, items: items.map(typeConverters.NotebookCellOutputItem.from), outputId }); - }, - - async replaceOutputItems(items: vscode.NotebookCellOutputItem | vscode.NotebookCellOutputItem[], outputId: string): Promise { - that.verifyStateForOutput(); - items = Array.isArray(items) ? items : [items]; - return that.applyEditSoon({ editType: CellEditType.OutputItems, items: items.map(typeConverters.NotebookCellOutputItem.from), outputId }); - }, - - token: that._tokenSource.token - }); - } -} - -class TimeoutBasedCollector { - private batch: T[] = []; - private waitPromise: Promise | undefined; - - constructor( - private readonly delay: number, - private readonly callback: (items: T[]) => Promise) { } - - addItem(item: T): Promise { - this.batch.push(item); - if (!this.waitPromise) { - this.waitPromise = timeout(this.delay).then(() => { - this.waitPromise = undefined; - const batch = this.batch; - this.batch = []; - return this.callback(batch); - }); - } - - return this.waitPromise; - } } diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index 8d8cdb712b..d7d27a3f02 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -6,12 +6,12 @@ import { Schemas } from 'vs/base/common/network'; import { deepFreeze, equals } from 'vs/base/common/objects'; import { URI } from 'vs/base/common/uri'; -import { CellKind, INotebookDocumentPropertiesChangeData, MainThreadNotebookDocumentsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { INotebookDocumentPropertiesChangeData, MainThreadNotebookDocumentsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostDocumentsAndEditors, IExtHostModelAddedData } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import * as extHostTypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; -import { IMainCellDto, IOutputDto, IOutputItemDto, NotebookCellMetadata, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookCellsSplice2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, IMainCellDto, IOutputDto, IOutputItemDto, NotebookCellInternalMetadata, NotebookCellMetadata, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookCellsSplice2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import * as vscode from 'vscode'; class RawContentChangeEvent { @@ -44,19 +44,19 @@ export class ExtHostCell { }; } - private _outputs: extHostTypes.NotebookCellOutput[]; - private _metadata: extHostTypes.NotebookCellMetadata; + private _outputs: vscode.NotebookCellOutput[]; + private _metadata: NotebookCellMetadata; private _previousResult: vscode.NotebookCellExecutionSummary | undefined; - private _internalMetadata: NotebookCellMetadata; + private _internalMetadata: NotebookCellInternalMetadata; readonly handle: number; readonly uri: URI; readonly cellKind: CellKind; - private _cell: vscode.NotebookCell | undefined; + private _apiCell: vscode.NotebookCell | undefined; constructor( - private readonly _notebook: ExtHostNotebookDocument, + readonly notebook: ExtHostNotebookDocument, private readonly _extHostDocument: ExtHostDocumentsAndEditors, private readonly _cellData: IMainCellDto, ) { @@ -64,33 +64,33 @@ export class ExtHostCell { this.uri = URI.revive(_cellData.uri); this.cellKind = _cellData.cellKind; this._outputs = _cellData.outputs.map(extHostTypeConverters.NotebookCellOutput.to); - this._internalMetadata = _cellData.metadata ?? {}; - this._metadata = extHostTypeConverters.NotebookCellMetadata.to(this._internalMetadata); - this._previousResult = extHostTypeConverters.NotebookCellPreviousExecutionResult.to(this._internalMetadata); + this._internalMetadata = _cellData.internalMetadata ?? {}; + this._metadata = _cellData.metadata ?? {}; + this._previousResult = extHostTypeConverters.NotebookCellExecutionSummary.to(_cellData.internalMetadata ?? {}); } - get internalMetadata(): NotebookCellMetadata { + get internalMetadata(): NotebookCellInternalMetadata { return this._internalMetadata; } get apiCell(): vscode.NotebookCell { - if (!this._cell) { + if (!this._apiCell) { const that = this; 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.apiNotebook, + this._apiCell = Object.freeze({ + get index() { return that.notebook.getCellIndex(that); }, + notebook: that.notebook.apiNotebook, kind: extHostTypeConverters.NotebookCellKind.to(this._cellData.cellKind), document: data.document, get outputs() { return that._outputs.slice(0); }, get metadata() { return that._metadata; }, - get latestExecutionSummary() { return that._previousResult; } + get executionSummary() { return that._previousResult; } }); } - return this._cell; + return this._apiCell; } setOutputs(newOutputs: IOutputDto[]): void { @@ -102,16 +102,19 @@ export class ExtHostCell { const output = this._outputs.find(op => op.id === outputId); if (output) { if (!append) { - output.outputs.length = 0; + output.items.length = 0; } - output.outputs.push(...newItems); + output.items.push(...newItems); } } setMetadata(newMetadata: NotebookCellMetadata): void { - this._internalMetadata = newMetadata; - this._metadata = extHostTypeConverters.NotebookCellMetadata.to(newMetadata); - this._previousResult = extHostTypeConverters.NotebookCellPreviousExecutionResult.to(newMetadata); + this._metadata = newMetadata; + } + + setInternalMetadata(newInternalMetadata: NotebookCellInternalMetadata): void { + this._internalMetadata = newInternalMetadata; + this._previousResult = extHostTypeConverters.NotebookCellExecutionSummary.to(newInternalMetadata); } } @@ -141,8 +144,8 @@ export class ExtHostNotebookDocument { private readonly _textDocumentsAndEditors: ExtHostDocumentsAndEditors, private readonly _textDocuments: ExtHostDocuments, private readonly _emitter: INotebookEventEmitter, - private readonly _viewType: string, - private _metadata: extHostTypes.NotebookDocumentMetadata, + private readonly _notebookType: string, + private _metadata: Record, readonly uri: URI, ) { } @@ -156,7 +159,7 @@ export class ExtHostNotebookDocument { this._notebook = { get uri() { return that.uri; }, get version() { return that._versionId; }, - get viewType() { return that._viewType; }, + get notebookType() { return that._notebookType; }, get isDirty() { return that._isDirty; }, get isUntitled() { return that.uri.scheme === Schemas.untitled; }, get isClosed() { return that._disposed; }, @@ -190,7 +193,7 @@ export class ExtHostNotebookDocument { acceptDocumentPropertiesChanged(data: INotebookDocumentPropertiesChangeData) { if (data.metadata) { - this._metadata = this._metadata.with(data.metadata); + this._metadata = { ...this._metadata, ...data.metadata }; } } @@ -213,11 +216,14 @@ export class ExtHostNotebookDocument { this._changeCellLanguage(rawEvent.index, rawEvent.language); } else if (rawEvent.kind === NotebookCellsChangeType.ChangeCellMetadata) { this._changeCellMetadata(rawEvent.index, rawEvent.metadata); + } else if (rawEvent.kind === NotebookCellsChangeType.ChangeCellInternalMetadata) { + this._changeCellInternalMetadata(rawEvent.index, rawEvent.internalMetadata); } } } private _validateIndex(index: number): number { + index = index | 0; if (index < 0) { return 0; } else if (index >= this._cells.length) { @@ -228,13 +234,15 @@ export class ExtHostNotebookDocument { } private _validateRange(range: vscode.NotebookRange): vscode.NotebookRange { - if (range.start < 0) { - range = range.with({ start: 0 }); + let start = range.start | 0; + let end = range.end | 0; + if (start < 0) { + start = 0; } - if (range.end > this._cells.length) { - range = range.with({ end: this._cells.length }); + if (end > this._cells.length) { + end = this._cells.length; } - return range; + return range.with({ start, end }); } private _getCells(range: vscode.NotebookRange): ExtHostCell[] { @@ -250,7 +258,7 @@ export class ExtHostNotebookDocument { if (this._disposed) { return Promise.reject(new Error('Notebook has been closed')); } - return this._proxy.$trySaveDocument(this.uri); + return this._proxy.$trySaveNotebook(this.uri); } private _spliceNotebookCells(splices: NotebookCellsSplice2[], initialization: boolean): void { @@ -275,7 +283,7 @@ export class ExtHostNotebookDocument { const changeEvent = new RawContentChangeEvent(splice[0], splice[1], [], newCells); const deletedItems = this._cells.splice(splice[0], splice[1], ...newCells); - for (let cell of deletedItems) { + for (const cell of deletedItems) { removedCellDocuments.push(cell.uri); changeEvent.deletedItems.push(cell.apiCell); } @@ -331,7 +339,6 @@ export class ExtHostNotebookDocument { private _changeCellMetadata(index: number, newMetadata: NotebookCellMetadata): void { const cell = this._cells[index]; - const originalInternalMetadata = cell.internalMetadata; const originalExtMetadata = cell.apiCell.metadata; cell.setMetadata(newMetadata); const newExtMetadata = cell.apiCell.metadata; @@ -339,13 +346,24 @@ export class ExtHostNotebookDocument { if (!equals(originalExtMetadata, newExtMetadata)) { this._emitter.emitCellMetadataChange(deepFreeze({ document: this.apiNotebook, cell: cell.apiCell })); } + } - if (originalInternalMetadata.runState !== newMetadata.runState) { - const executionState = newMetadata.runState ?? extHostTypes.NotebookCellExecutionState.Idle; - this._emitter.emitCellExecutionStateChange(deepFreeze({ document: this.apiNotebook, cell: cell.apiCell, executionState })); + private _changeCellInternalMetadata(index: number, newInternalMetadata: NotebookCellInternalMetadata): void { + const cell = this._cells[index]; + + const originalInternalMetadata = cell.internalMetadata; + cell.setInternalMetadata(newInternalMetadata); + + if (originalInternalMetadata.runState !== newInternalMetadata.runState) { + const executionState = newInternalMetadata.runState ?? extHostTypes.NotebookCellExecutionState.Idle; + this._emitter.emitCellExecutionStateChange(deepFreeze({ document: this.apiNotebook, cell: cell.apiCell, state: executionState })); } } + getCellFromApiCell(apiCell: vscode.NotebookCell): ExtHostCell | undefined { + return this._cells.find(cell => cell.apiCell === apiCell); + } + getCellFromIndex(index: number): ExtHostCell | undefined { return this._cells[index]; } diff --git a/src/vs/workbench/api/common/extHostNotebookDocuments.ts b/src/vs/workbench/api/common/extHostNotebookDocuments.ts new file mode 100644 index 0000000000..eeef4413ff --- /dev/null +++ b/src/vs/workbench/api/common/extHostNotebookDocuments.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ExtHostNotebookDocumentsShape, INotebookDocumentPropertiesChangeData } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; +import { NotebookCellsChangedEventDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import type * as vscode from 'vscode'; + +export class ExtHostNotebookDocuments implements ExtHostNotebookDocumentsShape { + + private readonly _onDidChangeNotebookDocumentMetadata = new Emitter(); + readonly onDidChangeNotebookDocumentMetadata = this._onDidChangeNotebookDocumentMetadata.event; + + private _onDidSaveNotebookDocument = new Emitter(); + readonly onDidSaveNotebookDocument = this._onDidSaveNotebookDocument.event; + + constructor( + @ILogService private readonly _logService: ILogService, + private readonly _notebooksAndEditors: ExtHostNotebookController, + ) { } + + $acceptModelChanged(uri: UriComponents, event: NotebookCellsChangedEventDto, isDirty: boolean): void { + const document = this._notebooksAndEditors.getNotebookDocument(URI.revive(uri)); + document.acceptModelChanged(event, isDirty); + } + + $acceptDirtyStateChanged(uri: UriComponents, isDirty: boolean): void { + const document = this._notebooksAndEditors.getNotebookDocument(URI.revive(uri)); + document.acceptModelChanged({ rawEvents: [], versionId: document.apiNotebook.version }, isDirty); + } + + $acceptModelSaved(uri: UriComponents): void { + const document = this._notebooksAndEditors.getNotebookDocument(URI.revive(uri)); + this._onDidSaveNotebookDocument.fire(document.apiNotebook); + } + + $acceptDocumentPropertiesChanged(uri: UriComponents, data: INotebookDocumentPropertiesChangeData): void { + this._logService.debug('ExtHostNotebook#$acceptDocumentPropertiesChanged', uri.path, data); + const document = this._notebooksAndEditors.getNotebookDocument(URI.revive(uri)); + document.acceptDocumentPropertiesChanged(data); + if (data.metadata) { + this._onDidChangeNotebookDocumentMetadata.fire({ document: document.apiNotebook }); + } + } +} diff --git a/src/vs/workbench/api/common/extHostNotebookEditor.ts b/src/vs/workbench/api/common/extHostNotebookEditor.ts index a62798aa3a..cfab6e7c69 100644 --- a/src/vs/workbench/api/common/extHostNotebookEditor.ts +++ b/src/vs/workbench/api/common/extHostNotebookEditor.ts @@ -9,6 +9,7 @@ import * as extHostConverter from 'vs/workbench/api/common/extHostTypeConverters import { CellEditType, ICellEditOperation, ICellReplaceEdit } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import * as vscode from 'vscode'; import { ExtHostNotebookDocument } from './extHostNotebookDocument'; +import { illegalArgument } from 'vs/base/common/errors'; interface INotebookEditData { documentVersionId: number; @@ -40,7 +41,7 @@ class NotebookEditorCellEditBuilder implements vscode.NotebookEditorEdit { } } - replaceMetadata(value: vscode.NotebookDocumentMetadata): void { + replaceMetadata(value: { [key: string]: any }): void { this._throwIfFinalized(); this._collectedEdits.push({ editType: CellEditType.DocumentMetadata, @@ -48,7 +49,7 @@ class NotebookEditorCellEditBuilder implements vscode.NotebookEditorEdit { }); } - replaceCellMetadata(index: number, metadata: vscode.NotebookCellMetadata): void { + replaceCellMetadata(index: number, metadata: Record): void { this._throwIfFinalized(); this._collectedEdits.push({ editType: CellEditType.Metadata, @@ -73,6 +74,8 @@ class NotebookEditorCellEditBuilder implements vscode.NotebookEditorEdit { export class ExtHostNotebookEditor { + public static readonly apiEditorsToExtHost = new WeakMap(); + private _selections: vscode.NotebookRange[] = []; private _visibleRanges: vscode.NotebookRange[] = []; private _viewColumn?: vscode.ViewColumn; @@ -105,6 +108,13 @@ export class ExtHostNotebookEditor { get selections() { return that._selections; }, + set selections(value: vscode.NotebookRange[]) { + if (!Array.isArray(value) || !value.every(extHostTypes.NotebookRange.isNotebookRange)) { + throw illegalArgument('selections'); + } + that._selections = value; + that._trySetSelections(value); + }, get visibleRanges() { return that._visibleRanges; }, @@ -127,6 +137,8 @@ export class ExtHostNotebookEditor { return that.setDecorations(decorationType, range); } }; + + ExtHostNotebookEditor.apiEditorsToExtHost.set(this._editor, this); } return this._editor; } @@ -147,6 +159,10 @@ export class ExtHostNotebookEditor { this._selections = selections; } + private _trySetSelections(value: vscode.NotebookRange[]): void { + this._proxy.$trySetSelections(this.id, value.map(extHostConverter.NotebookRange.from)); + } + _acceptViewColumn(value: vscode.ViewColumn | undefined) { this._viewColumn = value; } diff --git a/src/vs/workbench/api/common/extHostNotebookEditors.ts b/src/vs/workbench/api/common/extHostNotebookEditors.ts new file mode 100644 index 0000000000..ce91f36736 --- /dev/null +++ b/src/vs/workbench/api/common/extHostNotebookEditors.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 { Emitter } from 'vs/base/common/event'; +import { IdGenerator } from 'vs/base/common/idGenerator'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ExtHostNotebookEditorsShape, INotebookEditorPropertiesChangeData, INotebookEditorViewColumnInfo, MainContext, MainThreadNotebookEditorsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; +import type * as vscode from 'vscode'; + +class NotebookEditorDecorationType { + + private static readonly _Keys = new IdGenerator('NotebookEditorDecorationType'); + + readonly value: vscode.NotebookEditorDecorationType; + + constructor(proxy: MainThreadNotebookEditorsShape, options: vscode.NotebookDecorationRenderOptions) { + const key = NotebookEditorDecorationType._Keys.nextId(); + proxy.$registerNotebookEditorDecorationType(key, typeConverters.NotebookDecorationRenderOptions.from(options)); + + this.value = { + key, + dispose() { + proxy.$removeNotebookEditorDecorationType(key); + } + }; + } +} + + +export class ExtHostNotebookEditors implements ExtHostNotebookEditorsShape { + + private readonly _onDidChangeNotebookEditorSelection = new Emitter(); + private readonly _onDidChangeNotebookEditorVisibleRanges = new Emitter(); + + readonly onDidChangeNotebookEditorSelection = this._onDidChangeNotebookEditorSelection.event; + readonly onDidChangeNotebookEditorVisibleRanges = this._onDidChangeNotebookEditorVisibleRanges.event; + + constructor( + @ILogService private readonly _logService: ILogService, + @IExtHostRpcService private readonly _extHostRpc: IExtHostRpcService, + private readonly _notebooksAndEditors: ExtHostNotebookController, + ) { + + } + + + createNotebookEditorDecorationType(options: vscode.NotebookDecorationRenderOptions): vscode.NotebookEditorDecorationType { + return new NotebookEditorDecorationType(this._extHostRpc.getProxy(MainContext.MainThreadNotebookEditors), options).value; + } + + $acceptEditorPropertiesChanged(id: string, data: INotebookEditorPropertiesChangeData): void { + this._logService.debug('ExtHostNotebook#$acceptEditorPropertiesChanged', id, data); + const editor = this._notebooksAndEditors.getEditorById(id); + // ONE: make all state updates + if (data.visibleRanges) { + editor._acceptVisibleRanges(data.visibleRanges.ranges.map(typeConverters.NotebookRange.to)); + } + if (data.selections) { + editor._acceptSelections(data.selections.selections.map(typeConverters.NotebookRange.to)); + } + + // TWO: send all events after states have been updated + if (data.visibleRanges) { + this._onDidChangeNotebookEditorVisibleRanges.fire({ + notebookEditor: editor.apiEditor, + visibleRanges: editor.apiEditor.visibleRanges + }); + } + if (data.selections) { + this._onDidChangeNotebookEditorSelection.fire(Object.freeze({ + notebookEditor: editor.apiEditor, + selections: editor.apiEditor.selections + })); + } + } + + $acceptEditorViewColumns(data: INotebookEditorViewColumnInfo): void { + for (const id in data) { + const editor = this._notebooksAndEditors.getEditorById(id); + editor._acceptViewColumn(typeConverters.ViewColumn.to(data[id])); + } + } +} diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index add0d63d66..092ea28951 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -1,11 +1,11 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Emitter } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { ExtHostNotebookKernelsShape, IMainContext, INotebookKernelDto2, MainContext, MainThreadNotebookKernelsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { ExtHostNotebookKernelsShape, IMainContext, INotebookKernelDto2, MainContext, MainThreadNotebookDocumentsShape, MainThreadNotebookKernelsShape } from 'vs/workbench/api/common/extHost.protocol'; import * as vscode from 'vscode'; import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; @@ -13,30 +13,42 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import * as extHostTypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { asWebviewUri } from 'vs/workbench/api/common/shared/webview'; +import { ResourceMap } from 'vs/base/common/map'; +import { timeout } from 'vs/base/common/async'; +import { ExtHostCell, ExtHostNotebookDocument } from 'vs/workbench/api/common/extHostNotebookDocument'; +import { CellEditType, IImmediateCellEditOperation, IOutputDto, NotebookCellExecutionState, NullablePartialNotebookCellInternalMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { asArray } from 'vs/base/common/arrays'; +import { ILogService } from 'vs/platform/log/common/log'; +import { NotebookCellOutput } from 'vs/workbench/api/common/extHostTypes'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; interface IKernelData { extensionId: ExtensionIdentifier, controller: vscode.NotebookController; onDidChangeSelection: Emitter<{ selected: boolean; notebook: vscode.NotebookDocument; }>; - onDidReceiveMessage: Emitter<{ editor: vscode.NotebookEditor, message: any }>; + onDidReceiveMessage: Emitter<{ editor: vscode.NotebookEditor, message: any; }>; + associatedNotebooks: ResourceMap; } export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { private readonly _proxy: MainThreadNotebookKernelsShape; + private readonly _activeExecutions = new ResourceMap(); private readonly _kernelData = new Map(); private _handlePool: number = 0; constructor( - mainContext: IMainContext, + private readonly _mainContext: IMainContext, private readonly _initData: IExtHostInitDataService, - private readonly _extHostNotebook: ExtHostNotebookController + private readonly _extHostNotebook: ExtHostNotebookController, + @ILogService private readonly _logService: ILogService, ) { - this._proxy = mainContext.getProxy(MainContext.MainThreadNotebookKernels); + this._proxy = _mainContext.getProxy(MainContext.MainThreadNotebookKernels); } - createNotebookController(extension: IExtensionDescription, id: string, viewType: string, label: string, handler?: vscode.NotebookExecuteHandler, preloads?: vscode.NotebookKernelPreload[]): vscode.NotebookController { + createNotebookController(extension: IExtensionDescription, id: string, viewType: string, label: string, handler?: (cells: vscode.NotebookCell[], notebook: vscode.NotebookDocument, controller: vscode.NotebookController) => void | Thenable, preloads?: vscode.NotebookRendererScript[]): vscode.NotebookController { for (let data of this._kernelData.values()) { if (data.controller.id === id && ExtensionIdentifier.equals(extension.identifier, data.extensionId)) { @@ -44,31 +56,33 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { } } + const handle = this._handlePool++; const that = this; + this._logService.trace(`NotebookController[${handle}], CREATED by ${extension.identifier.value}, ${id}`); + const _defaultExecutHandler = () => console.warn(`NO execute handler from notebook controller '${data.id}' of extension: '${extension.identifier}'`); let isDisposed = false; const commandDisposables = new DisposableStore(); - const onDidChangeSelection = new Emitter<{ selected: boolean, notebook: vscode.NotebookDocument }>(); - const onDidReceiveMessage = new Emitter<{ editor: vscode.NotebookEditor, message: any }>(); + const onDidChangeSelection = new Emitter<{ selected: boolean, notebook: vscode.NotebookDocument; }>(); + const onDidReceiveMessage = new Emitter<{ editor: vscode.NotebookEditor, message: any; }>(); const data: INotebookKernelDto2 = { id: `${extension.identifier.value}/${id}`, - viewType, + notebookType: viewType, extensionId: extension.identifier, extensionLocation: extension.extensionLocation, label: label || extension.identifier.value, - preloads: preloads ? preloads.map(extHostTypeConverters.NotebookKernelPreload.from) : [] + preloads: preloads ? preloads.map(extHostTypeConverters.NotebookRendererScript.from) : [] }; // - let _executeHandler: vscode.NotebookExecuteHandler = handler ?? _defaultExecutHandler; - let _interruptHandler: vscode.NotebookInterruptHandler | undefined; + let _executeHandler = handler ?? _defaultExecutHandler; + let _interruptHandler: ((this: vscode.NotebookController, notebook: vscode.NotebookDocument) => void | Thenable) | undefined; - // todo@jrieken the selector needs to be massaged this._proxy.$addKernel(handle, data).catch(err => { // this can happen when a kernel with that ID is already registered console.log(err); @@ -91,10 +105,13 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { }); }; + // notebook documents that are associated to this controller + const associatedNotebooks = new ResourceMap(); + const controller: vscode.NotebookController = { get id() { return id; }, - get viewType() { return data.viewType; }, - onDidChangeNotebookAssociation: onDidChangeSelection.event, + get notebookType() { return data.notebookType; }, + onDidChangeSelectedNotebooks: onDidChangeSelection.event, get label() { return data.label; }, @@ -123,15 +140,15 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { data.supportedLanguages = value; _update(); }, - get hasExecutionOrder() { - return data.hasExecutionOrder ?? false; + get supportsExecutionOrder() { + return data.supportsExecutionOrder ?? false; }, - set hasExecutionOrder(value) { - data.hasExecutionOrder = value; + set supportsExecutionOrder(value) { + data.supportsExecutionOrder = value; _update(); }, - get preloads() { - return data.preloads ? data.preloads.map(extHostTypeConverters.NotebookKernelPreload.to) : []; + get rendererScripts() { + return data.preloads ? data.preloads.map(extHostTypeConverters.NotebookRendererScript.to) : []; }, get executeHandler() { return _executeHandler; @@ -147,15 +164,19 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { data.supportsInterrupt = Boolean(value); _update(); }, - createNotebookCellExecutionTask(cell) { + createNotebookCellExecution(cell) { if (isDisposed) { throw new Error('notebook controller is DISPOSED'); } - //todo@jrieken - return that._extHostNotebook.createNotebookCellExecution(cell.notebook.uri, cell.index, data.id)!; + if (!associatedNotebooks.has(cell.notebook.uri)) { + that._logService.trace(`NotebookController[${handle}] NOT associated to notebook, associated to THESE notebooks:`, Array.from(associatedNotebooks.keys()).map(u => u.toString())); + throw new Error(`notebook controller is NOT associated to notebook: ${cell.notebook.uri.toString()}`); + } + return that._createNotebookCellExecution(cell); }, dispose: () => { if (!isDisposed) { + this._logService.trace(`NotebookController[${handle}], DISPOSED`); isDisposed = true; this._kernelData.delete(handle); commandDisposables.dispose(); @@ -164,30 +185,47 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { this._proxy.$removeKernel(handle); } }, - // --- ipc - onDidReceiveMessage: onDidReceiveMessage.event, - postMessage(message, editor) { - return that._proxy.$postMessage(handle, editor && that._extHostNotebook.getIdByEditor(editor), message); - }, - asWebviewUri(uri: URI) { - return asWebviewUri(that._initData.environment, String(handle), uri); - }, // --- priority updateNotebookAffinity(notebook, priority) { that._proxy.$updateNotebookPriority(handle, notebook.uri, priority); - } + }, + // --- ipc + onDidReceiveMessage: onDidReceiveMessage.event, + postMessage(message, editor) { + checkProposedApiEnabled(extension); + return that._proxy.$postMessage(handle, editor && that._extHostNotebook.getIdByEditor(editor), message); + }, + asWebviewUri(uri: URI) { + checkProposedApiEnabled(extension); + return asWebviewUri(uri, that._initData.remote); + }, }; - this._kernelData.set(handle, { extensionId: extension.identifier, controller, onDidChangeSelection, onDidReceiveMessage }); + this._kernelData.set(handle, { + extensionId: extension.identifier, + controller, + onDidReceiveMessage, + onDidChangeSelection, + associatedNotebooks + }); return controller; } - $acceptSelection(handle: number, uri: UriComponents, value: boolean): void { + $acceptNotebookAssociation(handle: number, uri: UriComponents, value: boolean): void { const obj = this._kernelData.get(handle); if (obj) { + // update data structure + const notebook = this._extHostNotebook.getNotebookDocument(URI.revive(uri))!; + if (value) { + obj.associatedNotebooks.set(notebook.uri, true); + } else { + obj.associatedNotebooks.delete(notebook.uri); + } + this._logService.trace(`NotebookController[${handle}] ASSOCIATE notebook`, notebook.uri.toString(), value); + // send event obj.onDidChangeSelection.fire({ selected: value, - notebook: this._extHostNotebook.lookupNotebookDocument(URI.revive(uri))!.apiNotebook + notebook: notebook.apiNotebook }); } } @@ -198,11 +236,7 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { // extension can dispose kernels in the meantime return; } - const document = this._extHostNotebook.lookupNotebookDocument(URI.revive(uri)); - if (!document) { - throw new Error('MISSING notebook'); - } - + const document = this._extHostNotebook.getNotebookDocument(URI.revive(uri)); const cells: vscode.NotebookCell[] = []; for (let cellHandle of handles) { const cell = document.getCell(cellHandle); @@ -212,9 +246,11 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { } try { + this._logService.trace(`NotebookController[${handle}] EXECUTE cells`, document.uri.toString(), cells.length); await obj.controller.executeHandler.call(obj.controller, cells, document.apiNotebook, obj.controller); } catch (err) { // + this._logService.error(`NotebookController[${handle}] execute cells FAILED`, err); console.error(err); } } @@ -225,24 +261,24 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { // extension can dispose kernels in the meantime return; } - const document = this._extHostNotebook.lookupNotebookDocument(URI.revive(uri)); - if (!document) { - throw new Error('MISSING notebook'); - } + + // cancel or interrupt depends on the controller. When an interrupt handler is used we + // don't trigger the cancelation token of executions. + const document = this._extHostNotebook.getNotebookDocument(URI.revive(uri)); if (obj.controller.interruptHandler) { await obj.controller.interruptHandler.call(obj.controller, document.apiNotebook); - } - // we do both? interrupt and cancellation or should we be selective? - for (let cellHandle of handles) { - const cell = document.getCell(cellHandle); - if (cell) { - this._extHostNotebook.cancelOneNotebookCellExecution(cell); + } else { + for (let cellHandle of handles) { + const cell = document.getCell(cellHandle); + if (cell) { + this._activeExecutions.get(cell.uri)?.cancel(); + } } } } - $acceptRendererMessage(handle: number, editorId: string, message: any): void { + $acceptKernelMessageFromRenderer(handle: number, editorId: string, message: any): void { const obj = this._kernelData.get(handle); if (!obj) { // extension can dispose kernels in the meantime @@ -250,10 +286,231 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { } const editor = this._extHostNotebook.getEditorById(editorId); - if (!editor) { - throw new Error(`send message for UNKNOWN editor: ${editorId}`); - } - obj.onDidReceiveMessage.fire(Object.freeze({ editor: editor.apiEditor, message })); } + + // --- + + _createNotebookCellExecution(cell: vscode.NotebookCell): vscode.NotebookCellExecution { + if (cell.index < 0) { + throw new Error('CANNOT execute cell that has been REMOVED from notebook'); + } + const notebook = this._extHostNotebook.getNotebookDocument(cell.notebook.uri); + const cellObj = notebook.getCellFromApiCell(cell); + if (!cellObj) { + throw new Error('invalid cell'); + } + if (this._activeExecutions.has(cellObj.uri)) { + throw new Error(`duplicate execution for ${cellObj.uri}`); + } + const execution = new NotebookCellExecutionTask(cellObj.notebook, cellObj, this._mainContext.getProxy(MainContext.MainThreadNotebookDocuments)); + this._activeExecutions.set(cellObj.uri, execution); + const listener = execution.onDidChangeState(() => { + if (execution.state === NotebookCellExecutionTaskState.Resolved) { + execution.dispose(); + listener.dispose(); + this._activeExecutions.delete(cellObj.uri); + } + }); + return execution.asApiObject(); + } +} + + +enum NotebookCellExecutionTaskState { + Init, + Started, + Resolved +} + +class NotebookCellExecutionTask extends Disposable { + private _onDidChangeState = new Emitter(); + readonly onDidChangeState = this._onDidChangeState.event; + + private _state = NotebookCellExecutionTaskState.Init; + get state(): NotebookCellExecutionTaskState { return this._state; } + + private readonly _tokenSource = this._register(new CancellationTokenSource()); + + private readonly _collector: TimeoutBasedCollector; + + private _executionOrder: number | undefined; + + constructor( + private readonly _document: ExtHostNotebookDocument, + private readonly _cell: ExtHostCell, + private readonly _proxy: MainThreadNotebookDocumentsShape + ) { + super(); + + this._collector = new TimeoutBasedCollector(10, edits => this.applyEdits(edits)); + + this._executionOrder = _cell.internalMetadata.executionOrder; + this.mixinMetadata({ + runState: NotebookCellExecutionState.Pending, + executionOrder: null + }); + } + + cancel(): void { + this._tokenSource.cancel(); + } + + private async applyEditSoon(edit: IImmediateCellEditOperation): Promise { + await this._collector.addItem(edit); + } + + private async applyEdits(edits: IImmediateCellEditOperation[]): Promise { + return this._proxy.$applyEdits(this._document.uri, edits, false); + } + + private verifyStateForOutput() { + if (this._state === NotebookCellExecutionTaskState.Init) { + throw new Error('Must call start before modifying cell output'); + } + + if (this._state === NotebookCellExecutionTaskState.Resolved) { + throw new Error('Cannot modify cell output after calling resolve'); + } + } + + private mixinMetadata(mixinMetadata: NullablePartialNotebookCellInternalMetadata) { + const edit: IImmediateCellEditOperation = { editType: CellEditType.PartialInternalMetadata, handle: this._cell.handle, internalMetadata: mixinMetadata }; + this.applyEdits([edit]); + } + + private cellIndexToHandle(cellOrCellIndex: vscode.NotebookCell | number | undefined): number { + let cell: ExtHostCell | undefined = this._cell; + if (typeof cellOrCellIndex === 'number') { + // todo@jrieken remove support for number shortly + cell = this._document.getCellFromIndex(cellOrCellIndex); + } else if (cellOrCellIndex) { + cell = this._document.getCellFromApiCell(cellOrCellIndex); + } + if (!cell) { + throw new Error('INVALID cell'); + } + return cell.handle; + } + + private validateAndConvertOutputs(items: vscode.NotebookCellOutput[]): IOutputDto[] { + return items.map(output => { + const newOutput = NotebookCellOutput.ensureUniqueMimeTypes(output.items, true); + if (newOutput === output.items) { + return extHostTypeConverters.NotebookCellOutput.from(output); + } + return extHostTypeConverters.NotebookCellOutput.from({ + items: newOutput, + id: output.id, + metadata: output.metadata + }); + }); + } + + private async updateOutputs(outputs: vscode.NotebookCellOutput | vscode.NotebookCellOutput[], cell: vscode.NotebookCell | number | undefined, append: boolean): Promise { + const handle = this.cellIndexToHandle(cell); + const outputDtos = this.validateAndConvertOutputs(asArray(outputs)); + return this.applyEditSoon({ editType: CellEditType.Output, handle, append, outputs: outputDtos }); + } + + private async updateOutputItems(items: vscode.NotebookCellOutputItem | vscode.NotebookCellOutputItem[], outputOrOutputId: vscode.NotebookCellOutput | string, append: boolean): Promise { + if (NotebookCellOutput.isNotebookCellOutput(outputOrOutputId)) { + outputOrOutputId = outputOrOutputId.id; + } + items = NotebookCellOutput.ensureUniqueMimeTypes(asArray(items), true); + return this.applyEditSoon({ editType: CellEditType.OutputItems, items: items.map(extHostTypeConverters.NotebookCellOutputItem.from), outputId: outputOrOutputId, append }); + } + + asApiObject(): vscode.NotebookCellExecution { + const that = this; + const result: vscode.NotebookCellExecution = { + get token() { return that._tokenSource.token; }, + get cell() { return that._cell.apiCell; }, + get executionOrder() { return that._executionOrder; }, + set executionOrder(v: number | undefined) { + that._executionOrder = v; + that.mixinMetadata({ + executionOrder: v + }); + }, + + start(startTime?: number): void { + if (that._state === NotebookCellExecutionTaskState.Resolved || that._state === NotebookCellExecutionTaskState.Started) { + throw new Error('Cannot call start again'); + } + + that._state = NotebookCellExecutionTaskState.Started; + that._onDidChangeState.fire(); + + that.mixinMetadata({ + runState: NotebookCellExecutionState.Executing, + runStartTime: startTime ?? null + }); + }, + + end(success: boolean | undefined, endTime?: number): void { + if (that._state === NotebookCellExecutionTaskState.Resolved) { + throw new Error('Cannot call resolve twice'); + } + + that._state = NotebookCellExecutionTaskState.Resolved; + that._onDidChangeState.fire(); + + that.mixinMetadata({ + runState: null, + lastRunSuccess: success ?? null, + runEndTime: endTime ?? null, + }); + }, + + clearOutput(cell?: vscode.NotebookCell | number): Thenable { + that.verifyStateForOutput(); + return that.updateOutputs([], cell, false); + }, + + appendOutput(outputs: vscode.NotebookCellOutput | vscode.NotebookCellOutput[], cell?: vscode.NotebookCell | number): Promise { + that.verifyStateForOutput(); + return that.updateOutputs(outputs, cell, true); + }, + + replaceOutput(outputs: vscode.NotebookCellOutput | vscode.NotebookCellOutput[], cell?: vscode.NotebookCell | number): Promise { + that.verifyStateForOutput(); + return that.updateOutputs(outputs, cell, false); + }, + + appendOutputItems(items: vscode.NotebookCellOutputItem | vscode.NotebookCellOutputItem[], output: vscode.NotebookCellOutput | string): Promise { + that.verifyStateForOutput(); + return that.updateOutputItems(items, output, true); + }, + + replaceOutputItems(items: vscode.NotebookCellOutputItem | vscode.NotebookCellOutputItem[], output: vscode.NotebookCellOutput | string): Promise { + that.verifyStateForOutput(); + return that.updateOutputItems(items, output, false); + } + }; + return Object.freeze(result); + } +} + +class TimeoutBasedCollector { + private batch: T[] = []; + private waitPromise: Promise | undefined; + + constructor( + private readonly delay: number, + private readonly callback: (items: T[]) => Promise) { } + + addItem(item: T): Promise { + this.batch.push(item); + if (!this.waitPromise) { + this.waitPromise = timeout(this.delay).then(() => { + this.waitPromise = undefined; + const batch = this.batch; + this.batch = []; + return this.callback(batch); + }); + } + + return this.waitPromise; + } } diff --git a/src/vs/workbench/api/common/extHostNotebookRenderers.ts b/src/vs/workbench/api/common/extHostNotebookRenderers.ts new file mode 100644 index 0000000000..f364432449 --- /dev/null +++ b/src/vs/workbench/api/common/extHostNotebookRenderers.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ExtHostNotebookRenderersShape, IMainContext, MainContext, MainThreadNotebookRenderersShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; +import { ExtHostNotebookEditor } from 'vs/workbench/api/common/extHostNotebookEditor'; +import * as vscode from 'vscode'; + +export class ExtHostNotebookRenderers implements ExtHostNotebookRenderersShape { + private readonly _rendererMessageEmitters = new Map>>(); + private readonly proxy: MainThreadNotebookRenderersShape; + + constructor(mainContext: IMainContext, private readonly _extHostNotebook: ExtHostNotebookController) { + this.proxy = mainContext.getProxy(MainContext.MainThreadNotebookRenderers); + } + + public $postRendererMessage(editorId: string, rendererId: string, message: unknown): void { + const editor = this._extHostNotebook.getEditorById(editorId); + this._rendererMessageEmitters.get(rendererId)?.fire({ editor: editor.apiEditor, message }); + } + + public createRendererMessaging(rendererId: string): vscode.NotebookRendererMessaging { + const messaging: vscode.NotebookRendererMessaging = { + onDidReceiveMessage: (...args) => + this.getOrCreateEmitterFor(rendererId).event(...args), + postMessage: (editor, message) => { + const extHostEditor = ExtHostNotebookEditor.apiEditorsToExtHost.get(editor); + if (!extHostEditor) { + throw new Error(`The first argument to postMessage() must be a NotebookEditor`); + } + + this.proxy.$postMessage(extHostEditor.id, rendererId, message); + }, + }; + + return messaging; + } + + private getOrCreateEmitterFor(rendererId: string) { + let emitter = this._rendererMessageEmitters.get(rendererId); + if (emitter) { + return emitter; + } + + emitter = new Emitter({ + onLastListenerRemove: () => { + emitter?.dispose(); + this._rendererMessageEmitters.delete(rendererId); + } + }); + + this._rendererMessageEmitters.set(rendererId, emitter); + + return emitter; + } +} diff --git a/src/vs/workbench/api/common/extHostStatusBar.ts b/src/vs/workbench/api/common/extHostStatusBar.ts index 1599f2f74f..fa562117e2 100644 --- a/src/vs/workbench/api/common/extHostStatusBar.ts +++ b/src/vs/workbench/api/common/extHostStatusBar.ts @@ -10,8 +10,10 @@ 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'; export class ExtHostStatusBarEntry implements vscode.StatusBarItem { + private static ID_GEN = 0; private static ALLOWED_BACKGROUND_COLORS = new Map( @@ -21,17 +23,20 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { #proxy: MainThreadStatusBarShape; #commands: CommandsConverter; - private _id: number; + private _entryId: number; + + private _extension?: IExtensionDescription; + + private _id?: string; private _alignment: number; private _priority?: number; + private _disposed: boolean = false; private _visible: boolean = false; - private _statusId: string; - private _statusName: string; - private _text: string = ''; private _tooltip?: string; + private _name?: string; private _color?: string | ThemeColor; private _backgroundColor?: ThemeColor; private readonly _internalCommandRegistration = new DisposableStore(); @@ -43,20 +48,23 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { private _timeoutHandle: any; private _accessibilityInformation?: vscode.AccessibilityInformation; - constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, id: string, name: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation) { + constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, extension: IExtensionDescription, id?: string, alignment?: ExtHostStatusBarAlignment, priority?: number); + constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, extension: IExtensionDescription | undefined, id: string, alignment?: ExtHostStatusBarAlignment, priority?: number); + constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, extension?: IExtensionDescription, id?: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number) { this.#proxy = proxy; this.#commands = commands; - this._id = ExtHostStatusBarEntry.ID_GEN++; - this._statusId = id; - this._statusName = name; + this._entryId = ExtHostStatusBarEntry.ID_GEN++; + + this._extension = extension; + + this._id = id; this._alignment = alignment; this._priority = priority; - this._accessibilityInformation = accessibilityInformation; } - public get id(): number { - return this._id; + public get id(): string { + return this._id ?? this._extension!.identifier.value; } public get alignment(): vscode.StatusBarAlignment { @@ -71,6 +79,10 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { return this._text; } + public get name(): string | undefined { + return this._name; + } + public get tooltip(): string | undefined { return this._tooltip; } @@ -96,6 +108,11 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { this.update(); } + public set name(name: string | undefined) { + this._name = name; + this.update(); + } + public set tooltip(tooltip: string | undefined) { this._tooltip = tooltip; this.update(); @@ -150,7 +167,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._entryId); } private update(): void { @@ -164,6 +181,28 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { this._timeoutHandle = setTimeout(() => { this._timeoutHandle = undefined; + // If the id is not set, derive it from the extension identifier, + // otherwise make sure to prefix it with the extension identifier + // to get a more unique value across extensions. + let id: string; + if (this._extension) { + if (this._id) { + id = `${this._extension.identifier.value}.${this._id}`; + } else { + id = this._extension.identifier.value; + } + } else { + id = this._id!; + } + + // If the name is not set, derive it from the extension descriptor + let name: string; + if (this._name) { + name = this._name; + } else { + name = localize('extensionLabel', "{0} (Extension)", this._extension!.displayName || this._extension!.name); + } + // If a background color is set, the foreground is determined let color = this._color; if (this._backgroundColor) { @@ -171,7 +210,7 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { } // Set to status bar - this.#proxy.$setEntry(this.id, this._statusId, this._statusName, this._text, this._tooltip, this._command?.internal, color, + this.#proxy.$setEntry(this._entryId, id, name, this._text, this._tooltip, this._command?.internal, color, this._backgroundColor, this._alignment === ExtHostStatusBarAlignment.Left ? MainThreadStatusBarAlignment.LEFT : MainThreadStatusBarAlignment.RIGHT, this._priority, this._accessibilityInformation); }, 0); @@ -189,7 +228,8 @@ class StatusBarMessage { private _messages: { message: string }[] = []; constructor(statusBar: ExtHostStatusBar) { - this._item = statusBar.createStatusBarEntry('status.extensionMessage', localize('status.extensionMessage', "Extension Status"), ExtHostStatusBarAlignment.Left, Number.MIN_VALUE); + this._item = statusBar.createStatusBarEntry(undefined, 'status.extensionMessage', ExtHostStatusBarAlignment.Left, Number.MIN_VALUE); + this._item.name = localize('status.extensionMessage', "Extension Status"); } dispose() { @@ -233,12 +273,13 @@ export class ExtHostStatusBar { this._statusMessage = new StatusBarMessage(this); } - 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); + createStatusBarEntry(extension: IExtensionDescription | undefined, id: string, alignment?: ExtHostStatusBarAlignment, priority?: number): vscode.StatusBarItem; + createStatusBarEntry(extension: IExtensionDescription, id?: string, alignment?: ExtHostStatusBarAlignment, priority?: number): vscode.StatusBarItem; + createStatusBarEntry(extension: IExtensionDescription, id: string, alignment?: ExtHostStatusBarAlignment, priority?: number): vscode.StatusBarItem { + return new ExtHostStatusBarEntry(this._proxy, this._commands, extension, id, alignment, priority); } setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): Disposable { - const d = this._statusMessage.setMessage(text); let handle: any; diff --git a/src/vs/workbench/api/common/extHostTask.ts b/src/vs/workbench/api/common/extHostTask.ts index 5f17f54c48..61fa1416a6 100644 --- a/src/vs/workbench/api/common/extHostTask.ts +++ b/src/vs/workbench/api/common/extHostTask.ts @@ -605,8 +605,6 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask public abstract $resolveVariables(uriComponents: UriComponents, toResolve: { process?: { name: string; cwd?: string; path?: string }, variables: string[] }): Promise<{ process?: string, variables: { [key: string]: string; } }>; - public abstract $getDefaultShellAndArgs(): Promise<{ shell: string, args: string[] | string | undefined }>; - private nextHandle(): number { return this._handleCounter++; } @@ -775,10 +773,6 @@ export class WorkerExtHostTask extends ExtHostTaskBase { return result; } - public $getDefaultShellAndArgs(): Promise<{ shell: string, args: string[] | string | undefined }> { - throw new Error('Not implemented'); - } - public async $jsonTasksSupported(): Promise { return false; } diff --git a/src/vs/workbench/api/common/extHostTelemetry.ts b/src/vs/workbench/api/common/extHostTelemetry.ts index 2d14c450b1..929d28ab7f 100644 --- a/src/vs/workbench/api/common/extHostTelemetry.ts +++ b/src/vs/workbench/api/common/extHostTelemetry.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 1a5e68d803..50c9d99159 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -5,13 +5,12 @@ import type * as vscode from 'vscode'; import { Event, Emitter } from 'vs/base/common/event'; -import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, IShellAndArgsDto, ITerminalDimensionsDto, ITerminalLinkDto, TerminalIdentifier } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; +import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, ITerminalDimensionsDto, ITerminalLinkDto, TerminalIdentifier } from 'vs/workbench/api/common/extHost.protocol'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; -import { Disposable as VSCodeDisposable, EnvironmentVariableMutatorType } from './extHostTypes'; +import { Disposable as VSCodeDisposable, EnvironmentVariableMutatorType, ThemeColor } from './extHostTypes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { localize } from 'vs/nls'; import { NotSupportedError } from 'vs/base/common/errors'; @@ -19,9 +18,10 @@ import { serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/ter import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { generateUuid } from 'vs/base/common/uuid'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { IShellLaunchConfigDto, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalEnvironment, ITerminalLaunchError, TerminalShellType } from 'vs/platform/terminal/common/terminal'; +import { IProcessReadyEvent, IShellLaunchConfigDto, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalProfile, TerminalIcon, TerminalShellType } from 'vs/platform/terminal/common/terminal'; import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; -import { ITerminalProfile } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { withNullAsUndefined } from 'vs/base/common/types'; export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, IDisposable { @@ -37,15 +37,22 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, ID onDidWriteTerminalData: Event; createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal; - createTerminalFromOptions(options: vscode.TerminalOptions, isFeatureTerminal?: boolean): vscode.Terminal; + createTerminalFromOptions(options: vscode.TerminalOptions, internalOptions?: ITerminalInternalOptions): vscode.Terminal; createExtensionTerminal(options: vscode.ExtensionTerminalOptions): vscode.Terminal; attachPtyToTerminal(id: number, pty: vscode.Pseudoterminal): void; - getDefaultShell(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string; - getDefaultShellArgs(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string[] | string; + getDefaultShell(useAutomationShell: boolean): string; + getDefaultShellArgs(useAutomationShell: boolean): string[] | string; registerLinkProvider(provider: vscode.TerminalLinkProvider): vscode.Disposable; + registerProfileProvider(id: string, provider: vscode.TerminalProfileProvider): vscode.Disposable; getEnvironmentVariableCollection(extension: IExtensionDescription, persistent?: boolean): vscode.EnvironmentVariableCollection; } +export interface ITerminalInternalOptions { + isFeatureTerminal?: boolean; + useShellEnvironment?: boolean; + isSplitTerminal?: boolean; +} + export const IExtHostTerminalService = createDecorator('IExtHostTerminalService'); export class ExtHostTerminal { @@ -114,29 +121,39 @@ export class ExtHostTerminal { } public async create( - shellPath?: string, - shellArgs?: string[] | string, - cwd?: string | URI, - env?: ITerminalEnvironment, - icon?: string, - initialText?: string, - waitOnExit?: boolean, - strictEnv?: boolean, - hideFromUser?: boolean, - isFeatureTerminal?: boolean, - isExtensionOwnedTerminal?: boolean + options: vscode.TerminalOptions, + internalOptions?: ITerminalInternalOptions, ): Promise { 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, icon, initialText, waitOnExit, strictEnv, hideFromUser, isFeatureTerminal, isExtensionOwnedTerminal }); + await this._proxy.$createTerminal(this._id, { + name: options.name, + shellPath: withNullAsUndefined(options.shellPath), + shellArgs: withNullAsUndefined(options.shellArgs), + cwd: withNullAsUndefined(options.cwd), + env: withNullAsUndefined(options.env), + icon: withNullAsUndefined(asTerminalIcon(options.iconPath)), + initialText: withNullAsUndefined(options.message), + strictEnv: withNullAsUndefined(options.strictEnv), + hideFromUser: withNullAsUndefined(options.hideFromUser), + isFeatureTerminal: withNullAsUndefined(internalOptions?.isFeatureTerminal), + isExtensionOwnedTerminal: true, + useShellEnvironment: withNullAsUndefined(internalOptions?.useShellEnvironment), + isSplitTerminal: withNullAsUndefined(internalOptions?.isSplitTerminal) + }); } - public async createExtensionTerminal(): Promise { + public async createExtensionTerminal(isSplitTerminal?: boolean, iconPath?: URI | { light: URI; dark: URI } | ThemeIcon): Promise { if (typeof this._id !== 'string') { throw new Error('Terminal has already been created'); } - await this._proxy.$createTerminal(this._id, { name: this._name, isExtensionCustomPtyTerminal: true }); + await this._proxy.$createTerminal(this._id, { + name: this._name, + isExtensionCustomPtyTerminal: true, + icon: iconPath, + isSplitTerminal + }); // At this point, the id has been set via `$acceptTerminalOpened` if (typeof this._id === 'string') { throw new Error('Terminal creation failed'); @@ -195,8 +212,8 @@ export class ExtHostPseudoterminal implements ITerminalChildProcess { public readonly onProcessData: Event = this._onProcessData.event; private readonly _onProcessExit = new Emitter(); public readonly onProcessExit: Event = this._onProcessExit.event; - private readonly _onProcessReady = new Emitter<{ pid: number, cwd: string }>(); - public get onProcessReady(): Event<{ pid: number, cwd: string }> { return this._onProcessReady.event; } + private readonly _onProcessReady = new Emitter(); + public get onProcessReady(): Event { return this._onProcessReady.event; } private readonly _onProcessTitleChanged = new Emitter(); public readonly onProcessTitleChanged: Event = this._onProcessTitleChanged.event; private readonly _onProcessOverrideDimensions = new Emitter(); @@ -259,6 +276,9 @@ export class ExtHostPseudoterminal implements ITerminalChildProcess { if (this._pty.onDidOverrideDimensions) { this._pty.onDidOverrideDimensions(e => this._onProcessOverrideDimensions.fire(e ? { cols: e.columns, rows: e.rows } : undefined)); // {{SQL CARBON EDIT}} strict-null-checks } + if (this._pty.onDidChangeName) { + this._pty.onDidChangeName(title => this._onProcessTitleChanged.fire(title)); + } this._pty.open(initialDimensions ? initialDimensions : undefined); @@ -289,9 +309,12 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I protected _extensionTerminalAwaitingStart: { [id: number]: { initialDimensions: ITerminalDimensionsDto | undefined } | undefined } = {}; protected _getTerminalPromises: { [id: number]: Promise } = {}; protected _environmentVariableCollections: Map = new Map(); + private _defaultProfile: ITerminalProfile | undefined; + private _defaultAutomationProfile: ITerminalProfile | undefined; private readonly _bufferer: TerminalDataBufferer; private readonly _linkProviders: Set = new Set(); + private readonly _profileProviders: Map = new Map(); private readonly _terminalLinkCache: Map> = new Map(); private readonly _terminalLinkCancellationSource: Map = new Map(); @@ -331,16 +354,22 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } public abstract createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal; - public abstract createTerminalFromOptions(options: vscode.TerminalOptions): vscode.Terminal; - public abstract getDefaultShell(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string; - public abstract getDefaultShellArgs(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string[] | string; - public abstract $getAvailableProfiles(configuredProfilesOnly: boolean): Promise; - public abstract $getDefaultShellAndArgs(useAutomationShell: boolean): Promise; + public abstract createTerminalFromOptions(options: vscode.TerminalOptions, internalOptions?: ITerminalInternalOptions): vscode.Terminal; - public createExtensionTerminal(options: vscode.ExtensionTerminalOptions): vscode.Terminal { + public getDefaultShell(useAutomationShell: boolean): string { + const profile = useAutomationShell ? this._defaultAutomationProfile : this._defaultProfile; + return profile?.path || ''; + } + + public getDefaultShellArgs(useAutomationShell: boolean): string[] | string { + const profile = useAutomationShell ? this._defaultAutomationProfile : this._defaultProfile; + return profile?.args || []; + } + + public createExtensionTerminal(options: vscode.ExtensionTerminalOptions, internalOptions?: ITerminalInternalOptions): vscode.Terminal { const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name); const p = new ExtHostPseudoterminal(options.pty); - terminal.createExtensionTerminal().then(id => { + terminal.createExtensionTerminal(internalOptions?.isSplitTerminal, asTerminalIcon(options.iconPath)).then(id => { const disposable = this._setupExtHostProcessListeners(id, p); this._terminalProcessDisposables[id] = disposable; }); @@ -363,7 +392,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I if (id === null) { this._activeTerminal = undefined; if (original !== this._activeTerminal) { - this._onDidChangeActiveTerminal.fire(this._activeTerminal.value); + this._onDidChangeActiveTerminal.fire(this._activeTerminal?.value); // {{SQL CARBON EDIT}} } return; } @@ -555,6 +584,34 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I }); } + public registerProfileProvider(id: string, provider: vscode.TerminalProfileProvider): vscode.Disposable { + if (this._profileProviders.has(id)) { + throw new Error(`Terminal profile provider "${id}" already registered`); + } + this._profileProviders.set(id, provider); + this._proxy.$registerProfileProvider(id); + return new VSCodeDisposable(() => { + this._profileProviders.delete(id); + this._proxy.$unregisterProfileProvider(id); + }); + } + + public async $createContributedProfileTerminal(id: string, isSplitTerminal: boolean): Promise { + const token = new CancellationTokenSource().token; + const options = await this._profileProviders.get(id)?.provideProfileOptions(token); + if (token.isCancellationRequested) { + return; + } + if (!options) { + throw new Error(`No terminal profile options provided for id "${id}"`); + } + if ('pty' in options) { + this.createExtensionTerminal(options, { isSplitTerminal }); + return; + } + this.createTerminalFromOptions(options, { isSplitTerminal }); + } + public async $provideLinks(terminalId: number, line: string): Promise { const terminal = this._getTerminalById(terminalId); if (!terminal) { @@ -686,6 +743,11 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I }); } + public $acceptDefaultProfile(profile: ITerminalProfile, automationProfile: ITerminalProfile): void { + this._defaultProfile = profile; + this._defaultAutomationProfile = automationProfile; + } + private _setEnvironmentVariableCollection(extensionIdentifier: string, collection: EnvironmentVariableCollection): void { this._environmentVariableCollections.set(extensionIdentifier, collection); collection.onDidChangeCollection(() => { @@ -771,23 +833,20 @@ export class WorkerExtHostTerminalService extends BaseExtHostTerminalService { throw new NotSupportedError(); } - public createTerminalFromOptions(options: vscode.TerminalOptions): vscode.Terminal { - throw new NotSupportedError(); - } - - public getDefaultShell(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string { - throw new NotSupportedError(); - } - - public getDefaultShellArgs(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string[] | string { - throw new NotSupportedError(); - } - - public $getAvailableProfiles(configuredProfilesOnly: boolean): Promise { - throw new NotSupportedError(); - } - - public async $getDefaultShellAndArgs(useAutomationShell: boolean): Promise { + public createTerminalFromOptions(options: vscode.TerminalOptions, internalOptions?: ITerminalInternalOptions): vscode.Terminal { throw new NotSupportedError(); } } + +function asTerminalIcon(iconPath?: vscode.Uri | { light: vscode.Uri; dark: vscode.Uri } | vscode.ThemeIcon): TerminalIcon | undefined { + if (!iconPath) { + return undefined; + } + if (!('id' in iconPath)) { + return iconPath; + } + return { + id: iconPath.id, + color: iconPath.color as ThemeColor + }; +} diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 71657d1679..fc92738ab6 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -666,7 +666,7 @@ export class TestItemFilteredWrapper extends TestItemImpl { } } - const nowMatches = this.children.size > 0 || this.actual.uri.toString() === this.filterDocument.uri.toString(); + const nowMatches = this.children.size > 0 || this.actual.uri?.toString() === this.filterDocument.uri.toString(); this._cachedMatchesFilter = nowMatches; if (nowMatches !== didMatch) { diff --git a/src/vs/workbench/api/common/extHostTestingPrivateApi.ts b/src/vs/workbench/api/common/extHostTestingPrivateApi.ts index 66394266b4..ec101d34d0 100644 --- a/src/vs/workbench/api/common/extHostTestingPrivateApi.ts +++ b/src/vs/workbench/api/common/extHostTestingPrivateApi.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Emitter } from 'vs/base/common/event'; diff --git a/src/vs/workbench/api/common/extHostTextEditor.ts b/src/vs/workbench/api/common/extHostTextEditor.ts index 20634cdcb1..3359d57515 100644 --- a/src/vs/workbench/api/common/extHostTextEditor.ts +++ b/src/vs/workbench/api/common/extHostTextEditor.ts @@ -15,6 +15,7 @@ import { EndOfLine, Position, Range, Selection, SnippetString, TextEditorLineNum import type * as vscode from 'vscode'; import { ILogService } from 'vs/platform/log/common/log'; import { Lazy } from 'vs/base/common/lazy'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; export class TextEditorDecorationType { @@ -22,9 +23,9 @@ export class TextEditorDecorationType { readonly value: vscode.TextEditorDecorationType; - constructor(proxy: MainThreadTextEditorsShape, options: vscode.DecorationRenderOptions) { + constructor(proxy: MainThreadTextEditorsShape, extension: IExtensionDescription, options: vscode.DecorationRenderOptions) { const key = TextEditorDecorationType._Keys.nextId(); - proxy.$registerTextEditorDecorationType(key, TypeConverters.DecorationRenderOptions.from(options)); + proxy.$registerTextEditorDecorationType(extension.identifier, key, TypeConverters.DecorationRenderOptions.from(options)); this.value = Object.freeze({ key, dispose() { diff --git a/src/vs/workbench/api/common/extHostTextEditors.ts b/src/vs/workbench/api/common/extHostTextEditors.ts index b4ff85384b..63c435bba0 100644 --- a/src/vs/workbench/api/common/extHostTextEditors.ts +++ b/src/vs/workbench/api/common/extHostTextEditors.ts @@ -11,6 +11,7 @@ import { ExtHostTextEditor, TextEditorDecorationType } from 'vs/workbench/api/co import * as TypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { TextEditorSelectionChangeKind } from 'vs/workbench/api/common/extHostTypes'; import type * as vscode from 'vscode'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; export class ExtHostEditors implements ExtHostEditorsShape { @@ -91,8 +92,8 @@ export class ExtHostEditors implements ExtHostEditorsShape { } } - createTextEditorDecorationType(options: vscode.DecorationRenderOptions): vscode.TextEditorDecorationType { - return new TextEditorDecorationType(this._proxy, options).value; + createTextEditorDecorationType(extension: IExtensionDescription, options: vscode.DecorationRenderOptions): vscode.TextEditorDecorationType { + return new TextEditorDecorationType(this._proxy, extension, options).value; } // --- called from main thread diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 83b4b8d45d..8c77b3738a 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -9,7 +9,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import * as marked from 'vs/base/common/marked/marked'; import { parse } from 'vs/base/common/marshalling'; import { cloneAndChange } from 'vs/base/common/objects'; -import { isDefined, isNumber, isString } from 'vs/base/common/types'; +import { isDefined, isEmptyObject, isNumber, isString } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; import { IPosition } from 'vs/editor/common/core/position'; @@ -545,7 +545,7 @@ export namespace WorkspaceEdit { resource: entry.uri, edit: entry.edit, notebookMetadata: entry.notebookMetadata, - notebookVersionId: extHostNotebooks?.lookupNotebookDocument(entry.uri)?.apiNotebook.version + notebookVersionId: extHostNotebooks?.getNotebookDocument(entry.uri, true)?.apiNotebook.version }); } else if (entry._type === types.FileEditType.CellOutput) { @@ -580,7 +580,7 @@ export namespace WorkspaceEdit { _type: extHostProtocol.WorkspaceEditType.Cell, metadata: entry.metadata, resource: entry.uri, - notebookVersionId: extHostNotebooks?.lookupNotebookDocument(entry.uri)?.apiNotebook.version, + notebookVersionId: extHostNotebooks?.getNotebookDocument(entry.uri, true)?.apiNotebook.version, edit: { editType: notebooks.CellEditType.Replace, index: entry.index, @@ -1130,37 +1130,35 @@ export namespace SignatureHelp { } } -export namespace InlineHint { +export namespace InlayHint { - export function from(hint: vscode.InlineHint): modes.InlineHint { + export function from(hint: vscode.InlayHint): modes.InlayHint { return { text: hint.text, - range: Range.from(hint.range), - kind: InlineHintKind.from(hint.kind ?? types.InlineHintKind.Other), - description: hint.description && MarkdownString.fromStrict(hint.description), + position: Position.from(hint.position), + kind: InlayHintKind.from(hint.kind ?? types.InlayHintKind.Other), whitespaceBefore: hint.whitespaceBefore, whitespaceAfter: hint.whitespaceAfter }; } - export function to(hint: modes.InlineHint): vscode.InlineHint { - const res = new types.InlineHint( + export function to(hint: modes.InlayHint): vscode.InlayHint { + const res = new types.InlayHint( hint.text, - Range.to(hint.range), - InlineHintKind.to(hint.kind) + Position.to(hint.position), + InlayHintKind.to(hint.kind) ); res.whitespaceAfter = hint.whitespaceAfter; res.whitespaceBefore = hint.whitespaceBefore; - res.description = htmlContent.isMarkdownString(hint.description) ? MarkdownString.to(hint.description) : hint.description; return res; } } -export namespace InlineHintKind { - export function from(kind: vscode.InlineHintKind): modes.InlineHintKind { +export namespace InlayHintKind { + export function from(kind: vscode.InlayHintKind): modes.InlayHintKind { return kind; } - export function to(kind: modes.InlineHintKind): vscode.InlineHintKind { + export function to(kind: modes.InlayHintKind): vscode.InlayHintKind { return kind; } } @@ -1417,49 +1415,20 @@ export namespace NotebookRange { } } -export namespace NotebookCellMetadata { - - export function to(data: notebooks.NotebookCellMetadata): types.NotebookCellMetadata { - return new types.NotebookCellMetadata().with({ - ...data, - ...{ - executionOrder: null, - lastRunSuccess: null, - runState: null, - runStartTime: null, - runStartTimeAdjustment: null, - runEndTime: null - } - }); - } -} - -export namespace NotebookDocumentMetadata { - - export function from(data: types.NotebookDocumentMetadata): notebooks.NotebookDocumentMetadata { - return data; - } - - export function to(data: notebooks.NotebookDocumentMetadata): types.NotebookDocumentMetadata { - return new types.NotebookDocumentMetadata().with(data); - } -} - -export namespace NotebookCellPreviousExecutionResult { - export function to(data: notebooks.NotebookCellMetadata): vscode.NotebookCellExecutionSummary { +export namespace NotebookCellExecutionSummary { + export function to(data: notebooks.NotebookCellInternalMetadata): vscode.NotebookCellExecutionSummary { return { - startTime: data.runStartTime, - endTime: data.runEndTime, + timing: typeof data.runStartTime === 'number' && typeof data.runEndTime === 'number' ? { startTime: data.runStartTime, endTime: data.runEndTime } : undefined, executionOrder: data.executionOrder, success: data.lastRunSuccess }; } - export function from(data: vscode.NotebookCellExecutionSummary): Partial { + export function from(data: vscode.NotebookCellExecutionSummary): Partial { return { lastRunSuccess: data.success, - runStartTime: data.startTime, - runEndTime: data.endTime, + runStartTime: data.timing?.startTime, + runEndTime: data.timing?.endTime, executionOrder: data.executionOrder }; } @@ -1468,8 +1437,8 @@ export namespace NotebookCellPreviousExecutionResult { export namespace NotebookCellKind { export function from(data: vscode.NotebookCellKind): notebooks.CellKind { switch (data) { - case types.NotebookCellKind.Markdown: - return notebooks.CellKind.Markdown; + case types.NotebookCellKind.Markup: + return notebooks.CellKind.Markup; case types.NotebookCellKind.Code: default: return notebooks.CellKind.Code; @@ -1478,8 +1447,8 @@ export namespace NotebookCellKind { export function to(data: notebooks.CellKind): vscode.NotebookCellKind { switch (data) { - case notebooks.CellKind.Markdown: - return types.NotebookCellKind.Markdown; + case notebooks.CellKind.Markup: + return types.NotebookCellKind.Markup; case notebooks.CellKind.Code: default: return types.NotebookCellKind.Code; @@ -1487,17 +1456,40 @@ export namespace NotebookCellKind { } } +export namespace NotebookData { + + export function from(data: vscode.NotebookData): notebooks.NotebookDataDto { + const res: notebooks.NotebookDataDto = { + metadata: data.metadata ?? Object.create(null), + cells: [], + }; + for (let cell of data.cells) { + types.NotebookCellData.validate(cell); + res.cells.push(NotebookCellData.from(cell)); + } + return res; + } + + export function to(data: notebooks.NotebookDataDto): vscode.NotebookData { + const res = new types.NotebookData( + data.cells.map(NotebookCellData.to), + ); + if (!isEmptyObject(data.metadata)) { + res.metadata = data.metadata; + } + return res; + } +} + export namespace NotebookCellData { export function from(data: vscode.NotebookCellData): notebooks.ICellDto2 { return { cellKind: NotebookCellKind.from(data.kind), - language: data.language, - source: data.source, - metadata: { - ...data.metadata, - ...NotebookCellPreviousExecutionResult.from(data.latestExecutionSummary ?? {}) - }, + language: data.languageId, + source: data.value, + metadata: data.metadata, + internalMetadata: NotebookCellExecutionSummary.from(data.executionSummary ?? {}), outputs: data.outputs ? data.outputs.map(NotebookCellOutput.from) : [] }; } @@ -1508,7 +1500,8 @@ export namespace NotebookCellData { data.source, data.language, data.outputs ? data.outputs.map(NotebookCellOutput.to) : undefined, - data.metadata ? NotebookCellMetadata.to(data.metadata) : undefined, + data.metadata, + data.internalMetadata ? NotebookCellExecutionSummary.to(data.internalMetadata) : undefined ); } } @@ -1517,21 +1510,20 @@ export namespace NotebookCellOutputItem { export function from(item: types.NotebookCellOutputItem): notebooks.IOutputItemDto { return { mime: item.mime, - value: item.value, - metadata: item.metadata + valueBytes: Array.from(item.data), //todo@jrieken this HACKY and SLOW... hoist VSBuffer instead }; } export function to(item: notebooks.IOutputItemDto): types.NotebookCellOutputItem { - return new types.NotebookCellOutputItem(item.mime, item.value, item.metadata); + return new types.NotebookCellOutputItem(new Uint8Array(item.valueBytes), item.mime); } } export namespace NotebookCellOutput { - export function from(output: types.NotebookCellOutput): notebooks.IOutputDto { + export function from(output: vscode.NotebookCellOutput): notebooks.IOutputDto { return { outputId: output.id, - outputs: output.outputs.map(NotebookCellOutputItem.from), + outputs: output.items.map(NotebookCellOutputItem.from), metadata: output.metadata }; } @@ -1644,35 +1636,22 @@ export namespace NotebookDocumentContentOptions { export function from(options: vscode.NotebookDocumentContentOptions | undefined): notebooks.TransientOptions { return { transientOutputs: options?.transientOutputs ?? false, - transientCellMetadata: { - ...options?.transientCellMetadata, - executionOrder: true, - runState: true, - runStartTime: true, - runStartTimeAdjustment: true, - runEndTime: true, - lastRunSuccess: true - }, + transientCellMetadata: options?.transientCellMetadata ?? {}, transientDocumentMetadata: options?.transientDocumentMetadata ?? {} }; } } -export namespace NotebookKernelPreload { - export function from(preload: vscode.NotebookKernelPreload): { uri: UriComponents; provides: string[] } { +export namespace NotebookRendererScript { + export function from(preload: vscode.NotebookRendererScript): { uri: UriComponents; provides: string[] } { return { uri: preload.uri, - provides: typeof preload.provides === 'string' - ? [preload.provides] - : preload.provides ?? [] - }; - } - export function to(preload: { uri: UriComponents; provides: string[] }): vscode.NotebookKernelPreload { - return { - uri: URI.revive(preload.uri), provides: preload.provides }; } + export function to(preload: { uri: UriComponents; provides: string[] }): vscode.NotebookRendererScript { + return new types.NotebookRendererScript(URI.revive(preload.uri), preload.provides); + } } export namespace TestMessage { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index e70c04617c..97a3724ea5 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3,13 +3,13 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesceInPlace, equals } from 'vs/base/common/arrays'; +import { asArray, coalesceInPlace, equals } from 'vs/base/common/arrays'; import { illegalArgument } from 'vs/base/common/errors'; import { IRelativePattern } from 'vs/base/common/glob'; import { isMarkdownString, MarkdownString as BaseMarkdownString } from 'vs/base/common/htmlContent'; import { ReadonlyMapView, ResourceMap } from 'vs/base/common/map'; -import { isFalsyOrWhitespace } from 'vs/base/common/strings'; -import { isStringArray } from 'vs/base/common/types'; +import { normalizeMimeType } from 'vs/base/common/mime'; +import { isArray, isStringArray } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from 'vs/platform/files/common/files'; @@ -612,7 +612,7 @@ export interface IFileCellEdit { _type: FileEditType.Cell; uri: URI; edit?: ICellEditOperation; - notebookMetadata?: vscode.NotebookDocumentMetadata; + notebookMetadata?: Record; metadata?: vscode.WorkspaceEditEntryMetadata; } @@ -631,7 +631,7 @@ export interface ICellOutputEdit { index: number; append: boolean; newOutputs?: NotebookCellOutput[]; - newMetadata?: vscode.NotebookCellMetadata; + newMetadata?: Record; metadata?: vscode.WorkspaceEditEntryMetadata; } @@ -674,7 +674,7 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { // --- notebook - replaceNotebookMetadata(uri: URI, value: vscode.NotebookDocumentMetadata, metadata?: vscode.WorkspaceEditEntryMetadata): void { + replaceNotebookMetadata(uri: URI, value: Record, metadata?: vscode.WorkspaceEditEntryMetadata): void { this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.DocumentMetadata, metadata: value }, notebookMetadata: value }); } @@ -707,7 +707,7 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { } } - replaceNotebookCellMetadata(uri: URI, index: number, cellMetadata: vscode.NotebookCellMetadata, metadata?: vscode.WorkspaceEditEntryMetadata): void { + replaceNotebookCellMetadata(uri: URI, index: number, cellMetadata: Record, metadata?: vscode.WorkspaceEditEntryMetadata): void { this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.PartialMetadata, index, metadata: cellMetadata } }); } @@ -1420,24 +1420,23 @@ export enum SignatureHelpTriggerKind { } -export enum InlineHintKind { +export enum InlayHintKind { Other = 0, Type = 1, Parameter = 2, } @es5ClassCompat -export class InlineHint { +export class InlayHint { text: string; - range: Range; - kind?: vscode.InlineHintKind; - description?: string | vscode.MarkdownString; + position: Position; + kind?: vscode.InlayHintKind; whitespaceBefore?: boolean; whitespaceAfter?: boolean; - constructor(text: string, range: Range, kind?: vscode.InlineHintKind) { + constructor(text: string, position: Position, kind?: vscode.InlayHintKind) { this.text = text; - this.range = range; + this.position = position; this.kind = kind; } } @@ -1548,6 +1547,29 @@ export class CompletionList { } } +@es5ClassCompat +export class InlineSuggestion implements vscode.InlineCompletionItem { + + text: string; + range?: Range; + command?: vscode.Command; + + constructor(text: string, range?: Range, command?: vscode.Command) { + this.text = text; + this.range = range; + this.command = command; + } +} + +@es5ClassCompat +export class InlineSuggestions implements vscode.InlineCompletionList { + items: vscode.InlineCompletionItem[]; + + constructor(items: vscode.InlineCompletionItem[]) { + this.items = items; + } +} + export enum ViewColumn { Active = -1, Beside = -2, @@ -2439,6 +2461,11 @@ export class EvaluatableExpression implements vscode.EvaluatableExpression { } } +export enum InlineCompletionTriggerKind { + Automatic = 0, + Explicit = 1, +} + @es5ClassCompat export class InlineValueText implements vscode.InlineValueText { readonly range: Range; @@ -2956,103 +2983,20 @@ export class NotebookRange { } } -export class NotebookCellMetadata { - readonly inputCollapsed?: boolean; - readonly outputCollapsed?: boolean; - readonly [key: string]: any; - - constructor(inputCollapsed?: boolean, outputCollapsed?: boolean); - constructor(data: Record); - constructor(inputCollapsedOrData: (boolean | undefined) | Record, outputCollapsed?: boolean) { - if (typeof inputCollapsedOrData === 'object') { - Object.assign(this, inputCollapsedOrData); - } else { - this.inputCollapsed = inputCollapsedOrData; - this.outputCollapsed = outputCollapsed; - } - } - - with(change: { - inputCollapsed?: boolean | null, - outputCollapsed?: boolean | null, - [key: string]: any - }): NotebookCellMetadata { - - let { inputCollapsed, outputCollapsed, ...remaining } = change; - - if (inputCollapsed === undefined) { - inputCollapsed = this.inputCollapsed; - } else if (inputCollapsed === null) { - inputCollapsed = undefined; - } - if (outputCollapsed === undefined) { - outputCollapsed = this.outputCollapsed; - } else if (outputCollapsed === null) { - outputCollapsed = undefined; - } - - if (inputCollapsed === this.inputCollapsed && - outputCollapsed === this.outputCollapsed && - Object.keys(remaining).length === 0 - ) { - return this; - } - - return new NotebookCellMetadata( - { - inputCollapsed, - outputCollapsed, - ...remaining - } - ); - } -} - -export class NotebookDocumentMetadata { - readonly trusted: boolean; - readonly [key: string]: any; - - constructor(trusted?: boolean); - constructor(data: Record); - constructor(trustedOrData: boolean | Record = true) { - if (typeof trustedOrData === 'object') { - Object.assign(this, trustedOrData); - this.trusted = trustedOrData.trusted ?? true; - } else { - this.trusted = trustedOrData; - } - } - - with(change: { - trusted?: boolean | null, - [key: string]: any - }): NotebookDocumentMetadata { - - let { trusted, ...remaining } = change; - - if (trusted === undefined) { - trusted = this.trusted; - } else if (trusted === null) { - trusted = undefined; - } - - if (trusted === this.trusted && - Object.keys(remaining).length === 0 - ) { - return this; - } - - return new NotebookDocumentMetadata( - { - trusted, - ...remaining - } - ); - } -} - export class NotebookCellData { + static validate(data: NotebookCellData): void { + if (typeof data.kind !== 'number') { + throw new Error('NotebookCellData MUST have \'kind\' property'); + } + if (typeof data.value !== 'string') { + throw new Error('NotebookCellData MUST have \'value\' property'); + } + if (typeof data.languageId !== 'string') { + throw new Error('NotebookCellData MUST have \'languageId\' property'); + } + } + static isNotebookCellDataArray(value: unknown): value is vscode.NotebookCellData[] { return Array.isArray(value) && (value).every(elem => NotebookCellData.isNotebookCellData(elem)); } @@ -3063,30 +3007,31 @@ export class NotebookCellData { } kind: NotebookCellKind; - source: string; - language: string; - outputs?: NotebookCellOutput[]; - metadata?: NotebookCellMetadata; - latestExecutionSummary?: vscode.NotebookCellExecutionSummary; + value: string; + languageId: string; + outputs?: vscode.NotebookCellOutput[]; + metadata?: Record; + executionSummary?: vscode.NotebookCellExecutionSummary; - constructor(kind: NotebookCellKind, source: string, language: string, outputs?: NotebookCellOutput[], metadata?: NotebookCellMetadata, latestExecutionSummary?: vscode.NotebookCellExecutionSummary) { + constructor(kind: NotebookCellKind, value: string, languageId: string, outputs?: vscode.NotebookCellOutput[], metadata?: Record, executionSummary?: vscode.NotebookCellExecutionSummary) { this.kind = kind; - this.source = source; - this.language = language; + this.value = value; + this.languageId = languageId; this.outputs = outputs ?? []; this.metadata = metadata; - this.latestExecutionSummary = latestExecutionSummary; + this.executionSummary = executionSummary; + + NotebookCellData.validate(this); } } export class NotebookData { cells: NotebookCellData[]; - metadata: NotebookDocumentMetadata; + metadata?: { [key: string]: any }; - constructor(cells: NotebookCellData[], metadata?: NotebookDocumentMetadata) { + constructor(cells: NotebookCellData[]) { this.cells = cells; - this.metadata = metadata ?? new NotebookDocumentMetadata(); } } @@ -3094,32 +3039,105 @@ export class NotebookData { export class NotebookCellOutputItem { static isNotebookCellOutputItem(obj: unknown): obj is vscode.NotebookCellOutputItem { - return obj instanceof NotebookCellOutputItem; + if (obj instanceof NotebookCellOutputItem) { + return true; + } + if (!obj) { + return false; + } + return typeof (obj).mime === 'string' + && (obj).data instanceof Uint8Array; + } + + static error(err: Error | { name: string, message?: string, stack?: string }): NotebookCellOutputItem { + const obj = { + name: err.name, + message: err.message, + stack: err.stack + }; + return NotebookCellOutputItem.json(obj, 'application/vnd.code.notebook.error'); + } + + static stdout(value: string): NotebookCellOutputItem { + return NotebookCellOutputItem.text(value, 'application/vnd.code.notebook.stdout'); + } + + static stderr(value: string): NotebookCellOutputItem { + return NotebookCellOutputItem.text(value, 'application/vnd.code.notebook.stderr'); + } + + static bytes(value: Uint8Array, mime: string = 'application/octet-stream'): NotebookCellOutputItem { + return new NotebookCellOutputItem(value, mime); + } + + static #encoder = new TextEncoder(); + + static text(value: string, mime: string = 'text/plain'): NotebookCellOutputItem { + const bytes = NotebookCellOutputItem.#encoder.encode(String(value)); + return new NotebookCellOutputItem(bytes, mime); + } + + static json(value: any, mime: string = 'application/json'): NotebookCellOutputItem { + const rawStr = JSON.stringify(value, undefined, '\t'); + return NotebookCellOutputItem.text(rawStr, mime); } constructor( + public data: Uint8Array, public mime: string, - public value: unknown, // JSON'able - public metadata?: Record ) { - if (isFalsyOrWhitespace(this.mime)) { - throw new Error('INVALID mime type, must not be empty or falsy'); + const mimeNormalized = normalizeMimeType(mime, true); + if (!mimeNormalized) { + throw new Error('INVALID mime type, must not be empty or falsy: ' + mime); } + this.mime = mimeNormalized; } } export class NotebookCellOutput { + static isNotebookCellOutput(candidate: any): candidate is vscode.NotebookCellOutput { + if (candidate instanceof NotebookCellOutput) { + return true; + } + if (!candidate || typeof candidate !== 'object') { + return false; + } + return typeof (candidate).id === 'string' && isArray((candidate).items); + } + + static ensureUniqueMimeTypes(items: NotebookCellOutputItem[], warn: boolean = false): NotebookCellOutputItem[] { + const seen = new Set(); + const removeIdx = new Set(); + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const normalMime = normalizeMimeType(item.mime); + if (!seen.has(normalMime)) { + seen.add(normalMime); + continue; + } + // duplicated mime types... first has won + removeIdx.add(i); + if (warn) { + console.warn(`DUPLICATED mime type '${item.mime}' will be dropped`); + } + } + if (removeIdx.size === 0) { + return items; + } + return items.filter((_item, index) => !removeIdx.has(index)); + } + id: string; - outputs: NotebookCellOutputItem[]; + items: NotebookCellOutputItem[]; metadata?: Record; constructor( - outputs: NotebookCellOutputItem[], + items: NotebookCellOutputItem[], idOrMetadata?: string | Record, metadata?: Record ) { - this.outputs = outputs; + this.items = NotebookCellOutput.ensureUniqueMimeTypes(items, true); if (typeof idOrMetadata === 'string') { this.id = idOrMetadata; this.metadata = metadata; @@ -3131,7 +3149,7 @@ export class NotebookCellOutput { } export enum NotebookCellKind { - Markdown = 1, + Markup = 1, Code = 2 } @@ -3156,11 +3174,7 @@ export enum NotebookEditorRevealType { export class NotebookCellStatusBarItem { constructor( public text: string, - public alignment: NotebookCellStatusBarAlignment, - public command?: string | vscode.Command, - public tooltip?: string, - public priority?: number, - public accessibilityInformation?: vscode.AccessibilityInformation) { } + public alignment: NotebookCellStatusBarAlignment) { } } @@ -3169,6 +3183,18 @@ export enum NotebookControllerAffinity { Preferred = 2 } +export class NotebookRendererScript { + + public provides: string[]; + + constructor( + public uri: vscode.Uri, + provides: string | string[] = [] + ) { + this.provides = asArray(provides); + } +} + //#endregion //#region Timeline @@ -3228,6 +3254,25 @@ export class LinkedEditingRanges { } } +//#region ports +export class PortAttributes { + private _port: number; + private _autoForwardAction: PortAutoForwardAction; + constructor(port: number, autoForwardAction: PortAutoForwardAction) { + this._port = port; + this._autoForwardAction = autoForwardAction; + } + + get port(): number { + return this._port; + } + + get autoForwardAction(): PortAutoForwardAction { + return this._autoForwardAction; + } +} +//#endregion ports + //#region Testing export enum TestResultState { Unset = 0, @@ -3282,7 +3327,7 @@ const rangeComparator = (a: vscode.Range | undefined, b: vscode.Range | undefine export class TestItemImpl implements vscode.TestItem { public readonly id!: string; - public readonly uri!: vscode.Uri; + public readonly uri!: vscode.Uri | undefined; public readonly children!: ReadonlyMap; public readonly parent!: TestItemImpl | undefined; @@ -3296,7 +3341,7 @@ export class TestItemImpl implements vscode.TestItem { /** Extension-owned resolve handler */ public resolveHandler?: (token: vscode.CancellationToken) => void; - constructor(id: string, public label: string, uri: vscode.Uri, public data: unknown) { + constructor(id: string, public label: string, uri: vscode.Uri | undefined, public data: unknown) { const api = getPrivateApiFor(this); Object.defineProperties(this, { @@ -3322,7 +3367,7 @@ export class TestItemImpl implements vscode.TestItem { range: testItemPropAccessor(api, 'range', undefined, rangeComparator), description: testItemPropAccessor(api, 'description', undefined, strictEqualComparator), runnable: testItemPropAccessor(api, 'runnable', true, strictEqualComparator), - debuggable: testItemPropAccessor(api, 'debuggable', true, strictEqualComparator), + debuggable: testItemPropAccessor(api, 'debuggable', false, strictEqualComparator), status: testItemPropAccessor(api, 'status', TestItemStatus.Resolved, strictEqualComparator), error: testItemPropAccessor(api, 'error', undefined, strictEqualComparator), }); diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index d145d893e5..84f1a74af1 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -10,9 +10,9 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions' import { normalizeVersion, parseVersion } from 'vs/platform/extensions/common/extensionValidator'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; -import { deserializeWebviewMessage } from 'vs/workbench/api/common/extHostWebviewMessaging'; +import { serializeWebviewMessage, deserializeWebviewMessage } from 'vs/workbench/api/common/extHostWebviewMessaging'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; +import { asWebviewUri, webviewGenericCspSource, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; import type * as vscode from 'vscode'; import * as extHostProtocol from './extHost.protocol'; @@ -69,12 +69,11 @@ export class ExtHostWebview implements vscode.Webview { public asWebviewUri(resource: vscode.Uri): vscode.Uri { this.#hasCalledAsWebviewUri = true; - return asWebviewUri(this.#initData, this.#handle, resource); + return asWebviewUri(resource, this.#initData.remote); } public get cspSource(): string { - return this.#initData.webviewCspSource - .replace('{{uuid}}', this.#handle); + return webviewGenericCspSource; } public get html(): string { @@ -110,7 +109,7 @@ export class ExtHostWebview implements vscode.Webview { if (this.#isDisposed) { return false; } - const serialized = serializeMessage(message, { serializeBuffersForPostMessage: this.#serializeBuffersForPostMessage }); + const serialized = serializeWebviewMessage(message, { serializeBuffersForPostMessage: this.#serializeBuffersForPostMessage }); return this.#proxy.$postMessage(this.#handle, serialized.message, ...serialized.buffers); } @@ -122,48 +121,14 @@ export class ExtHostWebview implements vscode.Webview { } export function shouldSerializeBuffersForPostMessage(extension: IExtensionDescription): boolean { - if (!extension.enableProposedApi) { - return false; - } - try { const version = normalizeVersion(parseVersion(extension.engines.vscode)); - return !!version && version.majorBase >= 1 && version.minorBase >= 56; + return !!version && version.majorBase >= 1 && version.minorBase >= 57; } catch { return false; } } -export function serializeMessage(message: any, options: { serializeBuffersForPostMessage?: boolean }): { message: string, buffers: VSBuffer[] } { - if (options.serializeBuffersForPostMessage) { - // Extract all ArrayBuffers from the message and replace them with references. - const vsBuffers: Array<{ original: ArrayBuffer, vsBuffer: VSBuffer }> = []; - - const replacer = (_key: string, value: any) => { - if (value && value instanceof ArrayBuffer) { - let index = vsBuffers.findIndex(x => x.original === value); - if (index === -1) { - const bytes = new Uint8Array(value); - const vsBuffer = VSBuffer.wrap(bytes); - index = vsBuffers.length; - vsBuffers.push({ original: value, vsBuffer }); - } - - return { - $$vscode_array_buffer_reference$$: true, - index, - }; - } - return value; - }; - - const serializedMessage = JSON.stringify(message, replacer); - return { message: serializedMessage, buffers: vsBuffers.map(x => x.vsBuffer) }; - } else { - return { message: JSON.stringify(message), buffers: [] }; - } -} - export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { private readonly _webviewProxy: extHostProtocol.MainThreadWebviewsShape; diff --git a/src/vs/workbench/api/common/extHostWebviewMessaging.ts b/src/vs/workbench/api/common/extHostWebviewMessaging.ts index 06e76ab5b5..413bb9949e 100644 --- a/src/vs/workbench/api/common/extHostWebviewMessaging.ts +++ b/src/vs/workbench/api/common/extHostWebviewMessaging.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { VSBuffer } from 'vs/base/common/buffer'; @@ -21,9 +21,9 @@ class ArrayBufferSet { export function serializeWebviewMessage( message: any, - transfer?: readonly ArrayBuffer[] + options: { serializeBuffersForPostMessage?: boolean } ): { message: string, buffers: VSBuffer[] } { - if (transfer) { + if (options.serializeBuffersForPostMessage) { // Extract all ArrayBuffers from the message and replace them with references. const arrayBuffers = new ArrayBufferSet(); diff --git a/src/vs/workbench/api/common/extHostWindow.ts b/src/vs/workbench/api/common/extHostWindow.ts index 10dfdcf98e..1bc44aed73 100644 --- a/src/vs/workbench/api/common/extHostWindow.ts +++ b/src/vs/workbench/api/common/extHostWindow.ts @@ -61,8 +61,6 @@ export class ExtHostWindow implements ExtHostWindowShape { async asExternalUri(uri: URI, options: IOpenUriOptions): Promise { if (isFalsyOrWhitespace(uri.scheme)) { return Promise.reject('Invalid scheme - cannot be empty'); - } else if (!new Set([Schemas.http, Schemas.https]).has(uri.scheme)) { - return Promise.reject(`Invalid scheme '${uri.scheme}'`); } const result = await this._proxy.$asExternalUri(uri, options); diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index a1cf03742c..139dc65d81 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -563,8 +563,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac } requestWorkspaceTrust(options?: vscode.WorkspaceTrustRequestOptions): Promise { - const promise = this._proxy.$requestWorkspaceTrust(options); - return options?.modal ? promise : Promise.resolve(this._trusted); + return this._proxy.$requestWorkspaceTrust(options); } $onDidGrantWorkspaceTrust(): void { diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index 7cede96669..968af1ca4a 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -24,6 +24,7 @@ interface IAPIMenu { readonly description: string; readonly proposed?: boolean; // defaults to false readonly supportsSubmenus?: boolean; // defaults to true + readonly deprecationMessage?: string; } const apiMenus: IAPIMenu[] = [ @@ -137,7 +138,8 @@ const apiMenus: IAPIMenu[] = [ id: MenuId.StatusBarWindowIndicatorMenu, description: localize('menus.statusBarWindowIndicator', "The window indicator menu in the status bar"), proposed: true, - supportsSubmenus: false + supportsSubmenus: false, + deprecationMessage: localize('menus.statusBarWindowIndicator.deprecated', "Use menu 'statusBar/remoteIndicator' instead."), }, { key: 'statusBar/remoteIndicator', @@ -185,6 +187,12 @@ const apiMenus: IAPIMenu[] = [ proposed: true }, */ + { + key: 'notebook/toolbar/right', + id: MenuId.NotebookRightToolbar, + description: localize('notebook.toolbar.right', "The contributed notebook right toolbar menu"), + proposed: true + }, { key: 'notebook/cell/title', id: MenuId.NotebookCellTitle, @@ -272,8 +280,15 @@ const apiMenus: IAPIMenu[] = [ key: 'dataGrid/item/context', id: MenuId.DataGridItemContext, description: locConstants.menusExtensionPointDataGridContext - } + }, // {{SQL CARBON EDIT}} end menu entries + { + key: 'editor/inlineCompletions/actions', + id: MenuId.InlineCompletionsActions, + description: localize('inlineCompletions.actions', "The actions shown when hovering on an inline completion"), + supportsSubmenus: false, + proposed: true + }, ]; namespace schema { @@ -461,6 +476,7 @@ namespace schema { type: 'object', properties: index(apiMenus, menu => menu.key, menu => ({ description: menu.proposed ? `(${localize('proposed', "Proposed API")}) ${menu.description}` : menu.description, + deprecationMessage: menu.deprecationMessage, type: 'array', items: menu.supportsSubmenus === false ? menuItem : { oneOf: [menuItem, submenuItem] } })), @@ -482,6 +498,7 @@ namespace schema { export interface IUserFriendlyCommand { command: string; title: string | ILocalizedString; + shortTitle?: string | ILocalizedString; enablement?: string; category?: string | ILocalizedString; icon?: IUserFriendlyIcon; @@ -501,6 +518,9 @@ namespace schema { if (!isValidLocalizedString(command.title, collector, 'title')) { return false; } + if (command.shortTitle && !isValidLocalizedString(command.shortTitle, collector, 'shortTitle')) { + return false; + } if (command.enablement && typeof command.enablement !== 'string') { collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'precondition')); return false; @@ -554,6 +574,10 @@ namespace schema { description: localize('vscode.extension.contributes.commandType.title', 'Title by which the command is represented in the UI'), type: 'string' }, + shortTitle: { + description: localize('vscode.extension.contributes.commandType.shortTitle', 'Short title by which the command is represented in the UI'), + type: 'string' + }, category: { description: localize('vscode.extension.contributes.commandType.category', '(Optional) Category string by the command is grouped in the UI'), type: 'string' @@ -611,7 +635,7 @@ commandsExtensionPoint.setHandler(extensions => { return; } - const { icon, enablement, category, title, command } = userFriendlyCommand; + const { icon, enablement, category, title, shortTitle, command } = userFriendlyCommand; let absoluteIcon: { dark: URI; light?: URI; } | ThemeIcon | undefined; if (icon) { @@ -632,6 +656,7 @@ commandsExtensionPoint.setHandler(extensions => { bucket.push({ id: command, title, + shortTitle: extension.description.enableProposedApi ? shortTitle : undefined, category, precondition: ContextKeyExpr.deserialize(enablement), icon: absoluteIcon diff --git a/src/vs/workbench/api/common/shared/tasks.ts b/src/vs/workbench/api/common/shared/tasks.ts index 6ed02f7464..e154869ce8 100644 --- a/src/vs/workbench/api/common/shared/tasks.ts +++ b/src/vs/workbench/api/common/shared/tasks.ts @@ -19,6 +19,7 @@ export interface TaskPresentationOptionsDTO { showReuseMessage?: boolean; clear?: boolean; group?: string; + close?: boolean; } export interface RunOptionsDTO { diff --git a/src/vs/workbench/api/common/shared/treeDataTransfer.ts b/src/vs/workbench/api/common/shared/treeDataTransfer.ts index 123d05adee..bd0317f0af 100644 --- a/src/vs/workbench/api/common/shared/treeDataTransfer.ts +++ b/src/vs/workbench/api/common/shared/treeDataTransfer.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { ITreeDataTransfer, ITreeDataTransferItem } from 'vs/workbench/common/views'; diff --git a/src/vs/workbench/api/common/shared/webview.ts b/src/vs/workbench/api/common/shared/webview.ts index 74a4e1253d..ed9635cd9d 100644 --- a/src/vs/workbench/api/common/shared/webview.ts +++ b/src/vs/workbench/api/common/shared/webview.ts @@ -3,28 +3,63 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import type * as vscode from 'vscode'; export interface WebviewInitData { - readonly isExtensionDevelopmentDebug: boolean; - readonly webviewResourceRoot: string; - readonly webviewCspSource: string; + readonly remote: { + readonly isRemote: boolean; + readonly authority: string | undefined + }; } +/** + * Root from which resources in webviews are loaded. + * + * This is hardcoded because we never expect to actually hit it. Instead these requests + * should always go to a service worker. + */ +export const webviewResourceBaseHost = 'vscode-webview.net'; + +export const webviewRootResourceAuthority = `vscode-resource.${webviewResourceBaseHost}`; + +export const webviewGenericCspSource = `https://*.${webviewResourceBaseHost}`; + +/** + * Construct a uri that can load resources inside a webview + * + * We encode the resource component of the uri so that on the main thread + * we know where to load the resource from (remote or truly local): + * + * ```txt + * ${scheme}+${resource-authority}.vscode-resource.vscode-webview.net/${path} + * ``` + * + * @param resource Uri of the resource to load. + * @param remoteInfo Optional information about the remote that specifies where `resource` should be resolved from. + */ export function asWebviewUri( - initData: WebviewInitData, - uuid: string, resource: vscode.Uri, + remoteInfo?: { authority: string | undefined, isRemote: boolean } ): vscode.Uri { - const uri = initData.webviewResourceRoot - // Make sure we preserve the scheme of the resource but convert it into a normal path segment - // The scheme is important as we need to know if we are requesting a local or a remote resource. - .replace('{{resource}}', resource.scheme + withoutScheme(resource)) - .replace('{{uuid}}', uuid); - return URI.parse(uri); -} + if (resource.scheme === Schemas.http || resource.scheme === Schemas.https) { + return resource; + } -function withoutScheme(resource: vscode.Uri): string { - return resource.toString().replace(/^\S+?:/, ''); + if (remoteInfo && remoteInfo.authority && remoteInfo.isRemote && resource.scheme === Schemas.file) { + resource = URI.from({ + scheme: Schemas.vscodeRemote, + authority: remoteInfo.authority, + path: resource.path, + }); + } + + return URI.from({ + scheme: Schemas.https, + authority: `${resource.scheme}+${resource.authority}.${webviewRootResourceAuthority}`, + path: resource.path, + fragment: resource.fragment, + query: resource.query, + }); } diff --git a/src/vs/workbench/api/node/extHost.node.services.ts b/src/vs/workbench/api/node/extHost.node.services.ts index 0224200c04..6719761975 100644 --- a/src/vs/workbench/api/node/extHost.node.services.ts +++ b/src/vs/workbench/api/node/extHost.node.services.ts @@ -7,12 +7,12 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExtHostOutputService2 } from 'vs/workbench/api/node/extHostOutputService'; import { ExtHostTerminalService } from 'vs/workbench/api/node/extHostTerminalService'; import { ExtHostTask } from 'vs/workbench/api/node/extHostTask'; -// import { ExtHostDebugService } from 'vs/workbench/api/node/extHostDebugService'; +// import { ExtHostDebugService } from 'vs/workbench/api/node/extHostDebugService'; {{SQL CARBON EDIT}} import { NativeExtHostSearch } from 'vs/workbench/api/node/extHostSearch'; import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService'; import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService'; import { ExtHostTunnelService } from 'vs/workbench/api/node/extHostTunnelService'; -// import { IExtHostDebugService } from 'vs/workbench/api/common/extHostDebugService'; +// import { IExtHostDebugService } from 'vs/workbench/api/common/extHostDebugService'; {{SQL CARBON EDIT}} import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { IExtHostOutputService } from 'vs/workbench/api/common/extHostOutput'; import { IExtHostSearch } from 'vs/workbench/api/common/extHostSearch'; @@ -30,7 +30,7 @@ import { ILogService } from 'vs/platform/log/common/log'; registerSingleton(IExtHostExtensionService, ExtHostExtensionService); registerSingleton(ILogService, ExtHostLogService); -// registerSingleton(IExtHostDebugService, ExtHostDebugService); +// registerSingleton(IExtHostDebugService, ExtHostDebugService); {{SQL CARBON EDIT}} registerSingleton(IExtHostOutputService, ExtHostOutputService2); registerSingleton(IExtHostSearch, NativeExtHostSearch); registerSingleton(IExtHostTask, ExtHostTask); diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index a7d49e4aef..ebf80b7e72 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -20,10 +20,11 @@ import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ExtHostDebugServiceBase, ExtHostDebugSession, ExtHostVariableResolverService } from 'vs/workbench/api/common/extHostDebugService'; import { ISignService } from 'vs/platform/sign/common/sign'; import { SignService } from 'vs/platform/sign/node/signService'; -import { hasChildProcesses, prepareCommand, runInExternalTerminal } from 'vs/workbench/contrib/debug/node/terminals'; import { IDisposable } from 'vs/base/common/lifecycle'; import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; import { createCancelablePromise, firstParallel } from 'vs/base/common/async'; +import { hasChildProcesses, prepareCommand, runInExternalTerminal } from 'vs/workbench/contrib/debug/node/terminals'; +import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; export class ExtHostDebugService extends ExtHostDebugServiceBase { @@ -38,9 +39,10 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { @IExtHostExtensionService extensionService: IExtHostExtensionService, @IExtHostDocumentsAndEditors editorsService: IExtHostDocumentsAndEditors, @IExtHostConfiguration configurationService: IExtHostConfiguration, - @IExtHostTerminalService private _terminalService: IExtHostTerminalService + @IExtHostTerminalService private _terminalService: IExtHostTerminalService, + @IExtHostEditorTabs editorTabs: IExtHostEditorTabs ) { - super(extHostRpcService, workspaceService, extensionService, editorsService, configurationService); + super(extHostRpcService, workspaceService, extensionService, editorsService, configurationService, editorTabs); } protected override createDebugAdapter(adapter: IAdapterDescriptor, session: ExtHostDebugSession): AbstractDebugAdapter | undefined { @@ -79,11 +81,13 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { } const configProvider = await this._configurationService.getConfigProvider(); - const shell = this._terminalService.getDefaultShell(true, configProvider); - const shellArgs = this._terminalService.getDefaultShellArgs(true, configProvider); + const shell = this._terminalService.getDefaultShell(true); + const shellArgs = this._terminalService.getDefaultShellArgs(true); + + const terminalName = args.title || nls.localize('debug.terminal.title', "Debug Process"); const shellConfig = JSON.stringify({ shell, shellArgs }); - let terminal = await this._integratedTerminalInstances.checkout(shellConfig); + let terminal = await this._integratedTerminalInstances.checkout(shellConfig, terminalName); let cwdForPrepareCommand: string | undefined; let giveShellTimeToInitialize = false; @@ -93,10 +97,13 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { shellPath: shell, shellArgs: shellArgs, cwd: args.cwd, - name: args.title || nls.localize('debug.terminal.title', "debuggee"), + name: terminalName, }; giveShellTimeToInitialize = true; - terminal = this._terminalService.createTerminalFromOptions(options, true); + terminal = this._terminalService.createTerminalFromOptions(options, { + isFeatureTerminal: true, + useShellEnvironment: true + }); this._integratedTerminalInstances.insert(terminal, shellConfig); } else { @@ -139,14 +146,13 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { return shellProcessId; } else if (args.kind === 'external') { - return runInExternalTerminal(args, await this._configurationService.getConfigProvider()); } return super.$runInTerminal(args, sessionId); } protected createVariableResolver(folders: vscode.WorkspaceFolder[], editorService: ExtHostDocumentsAndEditors, configurationService: ExtHostConfigProvider): AbstractVariableResolverService { - return new ExtHostVariableResolverService(folders, editorService, configurationService, this._workspaceService); + return new ExtHostVariableResolverService(folders, editorService, configurationService, this._editorTabs, this._workspaceService); } } @@ -158,9 +164,15 @@ class DebugTerminalCollection { private _terminalInstances = new Map(); - public async checkout(config: string) { + public async checkout(config: string, name: string) { const entries = [...this._terminalInstances.entries()]; const promises = entries.map(([terminal, termInfo]) => createCancelablePromise(async ct => { + + // Only allow terminals that match the title. See #123189 + if (terminal.name !== name) { + return null; + } + if (termInfo.lastUsedAt !== -1 && await hasChildProcesses(await terminal.processId)) { return null; } diff --git a/src/vs/workbench/api/node/extHostOutputService.ts b/src/vs/workbench/api/node/extHostOutputService.ts index 34d7e97ab4..154bfa2d5d 100644 --- a/src/vs/workbench/api/node/extHostOutputService.ts +++ b/src/vs/workbench/api/node/extHostOutputService.ts @@ -8,26 +8,27 @@ import type * as vscode from 'vscode'; import { URI } from 'vs/base/common/uri'; import { join } from 'vs/base/common/path'; import { toLocalISOString } from 'vs/base/common/date'; -import { SymlinkSupport } from 'vs/base/node/pfs'; -import { promises } from 'fs'; +import { Promises, SymlinkSupport } from 'vs/base/node/pfs'; 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'; import { MutableDisposable } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; import { createRotatingLogger } from 'vs/platform/log/node/spdlogLog'; -import { RotatingLogger } from 'spdlog'; +import { Logger } from 'spdlog'; import { ByteSize } from 'vs/platform/files/common/files'; class OutputAppender { - private appender: RotatingLogger; + static async create(name: string, file: string): Promise { + const appender = await createRotatingLogger(name, file, 30 * ByteSize.MB, 1); + appender.clearFormatters(); - constructor(name: string, readonly file: string) { - this.appender = createRotatingLogger(name, file, 30 * ByteSize.MB, 1); - this.appender.clearFormatters(); + return new OutputAppender(name, file, appender); } + private constructor(readonly name: string, readonly file: string, private readonly appender: Logger) { } + append(content: string): void { this.appender.critical(content); } @@ -38,7 +39,7 @@ class OutputAppender { } -export class ExtHostOutputChannelBackedByFile extends AbstractExtHostOutputChannel { +class ExtHostOutputChannelBackedByFile extends AbstractExtHostOutputChannel { private _appender: OutputAppender; @@ -109,11 +110,11 @@ export class ExtHostOutputService2 extends ExtHostOutputService { const outputDirPath = join(this._logsLocation.fsPath, `output_logging_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`); const exists = await SymlinkSupport.existsDirectory(outputDirPath); if (!exists) { - await promises.mkdir(outputDirPath, { recursive: true }); + await Promises.mkdir(outputDirPath, { recursive: true }); } const fileName = `${this._namePool++}-${name.replace(/[\\/:\*\?"<>\|]/g, '')}`; const file = URI.file(join(outputDirPath, `${fileName}.log`)); - const appender = new OutputAppender(fileName, file.fsPath); + const appender = await OutputAppender.create(fileName, file.fsPath); return new ExtHostOutputChannelBackedByFile(name, appender, this._proxy); } catch (error) { // Do not crash if logger cannot be created diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index d1ebb32643..b9e69e7b3a 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -14,6 +14,7 @@ import * as tasks from '../common/shared/tasks'; import { ExtHostVariableResolverService } from 'vs/workbench/api/common/extHostDebugService'; import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; +import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; @@ -22,7 +23,7 @@ import { ExtHostTaskBase, TaskHandleDTO, TaskDTO, CustomExecutionDTO, HandlerDat import { Schemas } from 'vs/base/common/network'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; -import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; export class ExtHostTask extends ExtHostTaskBase { private _variableResolver: ExtHostVariableResolverService | undefined; @@ -35,7 +36,8 @@ export class ExtHostTask extends ExtHostTaskBase { @IExtHostConfiguration configurationService: IExtHostConfiguration, @IExtHostTerminalService extHostTerminalService: IExtHostTerminalService, @ILogService logService: ILogService, - @IExtHostApiDeprecationService deprecationService: IExtHostApiDeprecationService + @IExtHostApiDeprecationService deprecationService: IExtHostApiDeprecationService, + @IExtHostEditorTabs private readonly editorTabs: IExtHostEditorTabs ) { super(extHostRpc, initData, workspaceService, editorService, configurationService, extHostTerminalService, logService, deprecationService); if (initData.remote.isRemote && initData.remote.authority) { @@ -124,11 +126,10 @@ export class ExtHostTask extends ExtHostTaskBase { return resolvedTaskDTO; } - private async getVariableResolver(workspaceFolders: vscode.WorkspaceFolder[]): Promise { if (this._variableResolver === undefined) { const configProvider = await this._configurationService.getConfigProvider(); - this._variableResolver = new ExtHostVariableResolverService(workspaceFolders, this._editorService, configProvider, this.workspaceService); + this._variableResolver = new ExtHostVariableResolverService(workspaceFolders, this._editorService, configProvider, this.editorTabs, this.workspaceService); } return this._variableResolver; } @@ -173,10 +174,6 @@ export class ExtHostTask extends ExtHostTaskBase { return result; } - public $getDefaultShellAndArgs(): Promise<{ shell: string, args: string[] | string | undefined }> { - return this._terminalService.$getDefaultShellAndArgs(true); - } - public async $jsonTasksSupported(): Promise { return true; } diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index eea2af7edb..6f8ae6826e 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -3,143 +3,27 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as platform from 'vs/base/common/platform'; -import { withNullAsUndefined } from 'vs/base/common/types'; import { generateUuid } from 'vs/base/common/uuid'; -import { getSystemShell, getSystemShellSync } from 'vs/base/node/shell'; -import { ILogService } from 'vs/platform/log/common/log'; -import { SafeConfigProvider } from 'vs/platform/terminal/common/terminal'; -import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IShellAndArgsDto } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostConfigProvider, ExtHostConfiguration, IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; -import { ExtHostVariableResolverService } from 'vs/workbench/api/common/extHostDebugService'; -import { ExtHostDocumentsAndEditors, IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import { BaseExtHostTerminalService, ExtHostTerminal } from 'vs/workbench/api/common/extHostTerminalService'; -import { ExtHostWorkspace, IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { ITerminalProfile } from 'vs/workbench/contrib/terminal/common/terminal'; -import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; -import { detectAvailableProfiles } from 'vs/workbench/contrib/terminal/node/terminalProfiles'; +import { BaseExtHostTerminalService, ExtHostTerminal, ITerminalInternalOptions } from 'vs/workbench/api/common/extHostTerminalService'; import type * as vscode from 'vscode'; export class ExtHostTerminalService extends BaseExtHostTerminalService { - private _variableResolver: ExtHostVariableResolverService | undefined; - private _variableResolverPromise: Promise; - private _lastActiveWorkspace: IWorkspaceFolder | undefined; - - private _defaultShell: string | undefined; - constructor( - @IExtHostRpcService extHostRpc: IExtHostRpcService, - @IExtHostConfiguration private _extHostConfiguration: ExtHostConfiguration, - @IExtHostWorkspace private _extHostWorkspace: ExtHostWorkspace, - @IExtHostDocumentsAndEditors private _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, - @ILogService private _logService: ILogService + @IExtHostRpcService extHostRpc: IExtHostRpcService ) { 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.OS, process.env as platform.IProcessEnvironment).then(s => this._defaultShell = s); - - this._updateLastActiveWorkspace(); - this._variableResolverPromise = this._updateVariableResolver(); - this._registerListeners(); } public createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal { - const terminal = new ExtHostTerminal(this._proxy, generateUuid(), { name, shellPath, shellArgs }, name); - this._terminals.push(terminal); - terminal.create(shellPath, shellArgs); - return terminal.value; + return this.createTerminalFromOptions({ name, shellPath, shellArgs }); } - public createTerminalFromOptions(options: vscode.TerminalOptions, isFeatureTerminal?: boolean): vscode.Terminal { + public createTerminalFromOptions(options: vscode.TerminalOptions, internalOptions?: ITerminalInternalOptions): vscode.Terminal { const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name); this._terminals.push(terminal); - terminal.create( - withNullAsUndefined(options.shellPath), - withNullAsUndefined(options.shellArgs), - withNullAsUndefined(options.cwd), - withNullAsUndefined(options.env), - withNullAsUndefined(options.icon), - withNullAsUndefined(options.message), - /*options.waitOnExit*/ undefined, - withNullAsUndefined(options.strictEnv), - withNullAsUndefined(options.hideFromUser), - withNullAsUndefined(isFeatureTerminal), - true - ); + terminal.create(options, internalOptions); return terminal.value; } - - public getDefaultShell(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string { - return terminalEnvironment.getDefaultShell( - this._buildSafeConfigProvider(configProvider), - this._defaultShell ?? getSystemShellSync(platform.OS, process.env as platform.IProcessEnvironment), - process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'), - process.env.windir, - terminalEnvironment.createVariableResolver(this._lastActiveWorkspace, process.env, this._variableResolver), - this._logService, - useAutomationShell - ); - } - - public getDefaultShellArgs(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string[] | string { - return terminalEnvironment.getDefaultShellArgs( - this._buildSafeConfigProvider(configProvider), - useAutomationShell, - terminalEnvironment.createVariableResolver(this._lastActiveWorkspace, process.env, this._variableResolver), - this._logService - ); - } - - private _registerListeners(): void { - this._extHostDocumentsAndEditors.onDidChangeActiveTextEditor(() => this._updateLastActiveWorkspace()); - this._extHostWorkspace.onDidChangeWorkspace(() => { - this._variableResolverPromise = this._updateVariableResolver(); - }); - } - - private _updateLastActiveWorkspace(): void { - const activeEditor = this._extHostDocumentsAndEditors.activeEditor(); - if (activeEditor) { - this._lastActiveWorkspace = this._extHostWorkspace.getWorkspaceFolder(activeEditor.document.uri) as IWorkspaceFolder; - } - } - - private async _updateVariableResolver(): Promise { - const configProvider = await this._extHostConfiguration.getConfigProvider(); - const workspaceFolders = await this._extHostWorkspace.getWorkspaceFolders2(); - this._variableResolver = new ExtHostVariableResolverService(workspaceFolders || [], this._extHostDocumentsAndEditors, configProvider); - return this._variableResolver; - } - - public async $getAvailableProfiles(configuredProfilesOnly: boolean): Promise { - const safeConfigProvider = this._buildSafeConfigProvider(await this._extHostConfiguration.getConfigProvider()); - return detectAvailableProfiles(configuredProfilesOnly, safeConfigProvider, undefined, this._logService, await this._variableResolverPromise, this._lastActiveWorkspace); - } - - public async $getDefaultShellAndArgs(useAutomationShell: boolean): Promise { - const configProvider = await this._extHostConfiguration.getConfigProvider(); - return { - shell: this.getDefaultShell(useAutomationShell, configProvider), - args: this.getDefaultShellArgs(useAutomationShell, configProvider) - }; - } - - // TODO: Remove when workspace trust is enabled - private _buildSafeConfigProvider(configProvider: ExtHostConfigProvider): SafeConfigProvider { - const config = configProvider.getConfiguration(); - return (key: string) => { - const isWorkspaceConfigAllowed = config.get('terminal.integrated.allowWorkspaceConfiguration'); - if (isWorkspaceConfigAllowed) { - return config.get(key) as any; - } - const inspected = config.inspect(key); - return inspected?.globalValue || inspected?.defaultValue; - }; - } } diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts index 96bfa04a2e..ea28a60c99 100644 --- a/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -11,7 +11,6 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData 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 * as types from 'vs/workbench/api/common/extHostTypes'; import { isLinux } from 'vs/base/common/platform'; @@ -365,8 +364,8 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe let tcp: string = ''; let tcp6: string = ''; try { - tcp = await fs.promises.readFile('/proc/net/tcp', 'utf8'); - tcp6 = await fs.promises.readFile('/proc/net/tcp6', 'utf8'); + tcp = await pfs.Promises.readFile('/proc/net/tcp', 'utf8'); + tcp6 = await pfs.Promises.readFile('/proc/net/tcp6', 'utf8'); } catch (e) { // File reading error. No additional handling needed. } @@ -379,7 +378,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe })); const socketMap = getSockets(procSockets); - const procChildren = await pfs.readdir('/proc'); + const procChildren = await pfs.Promises.readdir('/proc'); const processes: { pid: number, cwd: string, cmd: string }[] = []; @@ -387,10 +386,10 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe try { const pid: number = Number(childName); const childUri = resources.joinPath(URI.file('/proc'), childName); - const childStat = await fs.promises.stat(childUri.fsPath); + const childStat = await pfs.Promises.stat(childUri.fsPath); if (childStat.isDirectory() && !isNaN(pid)) { - const cwd = await fs.promises.readlink(resources.joinPath(childUri, 'cwd').fsPath); - const cmd = await fs.promises.readFile(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8'); + const cwd = await pfs.Promises.readlink(resources.joinPath(childUri, 'cwd').fsPath); + const cmd = await pfs.Promises.readFile(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8'); processes.push({ pid, cwd, cmd }); } } catch (e) { diff --git a/src/vs/workbench/browser/menuActions.ts b/src/vs/workbench/browser/actions.ts similarity index 99% rename from src/vs/workbench/browser/menuActions.ts rename to src/vs/workbench/browser/actions.ts index fd877df272..68029ae441 100644 --- a/src/vs/workbench/browser/menuActions.ts +++ b/src/vs/workbench/browser/actions.ts @@ -32,7 +32,9 @@ class MenuActions extends Disposable { private readonly contextKeyService: IContextKeyService ) { super(); + this.menu = this._register(menuService.createMenu(menuId, contextKeyService)); + this._register(this.menu.onDidChange(() => this.updateActions())); this.updateActions(); } @@ -48,6 +50,7 @@ class MenuActions extends Disposable { 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)); @@ -55,6 +58,7 @@ class MenuActions extends Disposable { disposables.add(this.updateSubmenus(action.actions, submenus)); } } + return disposables; } } @@ -75,7 +79,9 @@ export class CompositeMenuActions extends Disposable { @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())); } @@ -89,11 +95,13 @@ export class CompositeMenuActions extends Disposable { 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/actions/developerActions.ts b/src/vs/workbench/browser/actions/developerActions.ts index 0efd898808..4f27f91424 100644 --- a/src/vs/workbench/browser/actions/developerActions.ts +++ b/src/vs/workbench/browser/actions/developerActions.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/actions'; import { localize } from 'vs/nls'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { domEvent } from 'vs/base/browser/event'; +import { DomEmitter } from 'vs/base/browser/event'; import { Color } from 'vs/base/common/color'; import { Event } from 'vs/base/common/event'; import { IDisposable, toDisposable, dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -63,8 +63,8 @@ class InspectContextKeysAction extends Action2 { hoverFeedback.style.backgroundColor = 'rgba(255, 0, 0, 0.5)'; hoverFeedback.style.zIndex = '1000'; - const onMouseMove = domEvent(document.body, 'mousemove', true); - disposables.add(onMouseMove(e => { + const onMouseMove = disposables.add(new DomEmitter(document.body, 'mousemove', true)); + disposables.add(onMouseMove.event(e => { const target = e.target as HTMLElement; const position = getDomNodePagePosition(target); @@ -74,11 +74,11 @@ class InspectContextKeysAction extends Action2 { hoverFeedback.style.height = `${position.height}px`; })); - const onMouseDown = Event.once(domEvent(document.body, 'mousedown', true)); - onMouseDown(e => { e.preventDefault(); e.stopPropagation(); }, null, disposables); + const onMouseDown = disposables.add(new DomEmitter(document.body, 'mousedown', true)); + Event.once(onMouseDown.event)(e => { e.preventDefault(); e.stopPropagation(); }, null, disposables); - const onMouseUp = Event.once(domEvent(document.body, 'mouseup', true)); - onMouseUp(e => { + const onMouseUp = disposables.add(new DomEmitter(document.body, 'mouseup', true)); + Event.once(onMouseUp.event)(e => { e.preventDefault(); e.stopPropagation(); @@ -120,9 +120,9 @@ class ToggleScreencastModeAction extends Action2 { const mouseMarker = append(container, $('.screencast-mouse')); disposables.add(toDisposable(() => mouseMarker.remove())); - const onMouseDown = domEvent(container, 'mousedown', true); - const onMouseUp = domEvent(container, 'mouseup', true); - const onMouseMove = domEvent(container, 'mousemove', true); + const onMouseDown = disposables.add(new DomEmitter(container, 'mousedown', true)); + const onMouseUp = disposables.add(new DomEmitter(container, 'mouseup', true)); + const onMouseMove = disposables.add(new DomEmitter(container, 'mousemove', true)); const updateMouseIndicatorColor = () => { mouseMarker.style.borderColor = Color.fromHex(configurationService.getValue('screencastMode.mouseIndicatorColor')).toString(); @@ -139,17 +139,17 @@ class ToggleScreencastModeAction extends Action2 { updateMouseIndicatorColor(); updateMouseIndicatorSize(); - disposables.add(onMouseDown(e => { + disposables.add(onMouseDown.event(e => { mouseMarker.style.top = `${e.clientY - mouseIndicatorSize / 2}px`; mouseMarker.style.left = `${e.clientX - mouseIndicatorSize / 2}px`; mouseMarker.style.display = 'block'; - const mouseMoveListener = onMouseMove(e => { + const mouseMoveListener = onMouseMove.event(e => { mouseMarker.style.top = `${e.clientY - mouseIndicatorSize / 2}px`; mouseMarker.style.left = `${e.clientX - mouseIndicatorSize / 2}px`; }); - Event.once(onMouseUp)(() => { + Event.once(onMouseUp.event)(() => { mouseMarker.style.display = 'none'; mouseMoveListener.dispose(); }); @@ -197,11 +197,11 @@ class ToggleScreencastModeAction extends Action2 { } })); - const onKeyDown = domEvent(window, 'keydown', true); + const onKeyDown = disposables.add(new DomEmitter(window, 'keydown', true)); let keyboardTimeout: IDisposable = Disposable.None; let length = 0; - disposables.add(onKeyDown(e => { + disposables.add(onKeyDown.event(e => { keyboardTimeout.dispose(); const event = new StandardKeyboardEvent(e); diff --git a/src/vs/workbench/browser/actions/helpActions.ts b/src/vs/workbench/browser/actions/helpActions.ts index 766fa387a6..5e669a9216 100644 --- a/src/vs/workbench/browser/actions/helpActions.ts +++ b/src/vs/workbench/browser/actions/helpActions.ts @@ -9,7 +9,7 @@ import { isMacintosh, isLinux, language } from 'vs/base/common/platform'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { URI } from 'vs/base/common/uri'; -import { MenuId, Action2, registerAction2, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { MenuId, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { KeyChord, KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { IProductService } from 'vs/platform/product/common/productService'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -24,13 +24,22 @@ class KeybindingsReferenceAction extends Action2 { constructor() { super({ id: KeybindingsReferenceAction.ID, - title: { value: localize('keybindingsReference', "Keyboard Shortcuts Reference"), original: 'Keyboard Shortcuts Reference' }, + title: { + value: localize('keybindingsReference', "Keyboard Shortcuts Reference"), + mnemonicTitle: localize({ key: 'miKeyboardShortcuts', comment: ['&& denotes a mnemonic'] }, "&&Keyboard Shortcuts Reference"), + original: 'Keyboard Shortcuts Reference' + }, category: CATEGORIES.Help, f1: true, keybinding: { weight: KeybindingWeight.WorkbenchContrib, when: null, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_R) + }, + menu: { + id: MenuId.MenubarHelpMenu, + group: '2_reference', + order: 1 } }); } @@ -46,30 +55,6 @@ class KeybindingsReferenceAction extends Action2 { } } -class OpenDocumentationUrlAction extends Action2 { - - static readonly ID = 'workbench.action.openDocumentationUrl'; - static readonly AVAILABLE = !!product.documentationUrl; - - constructor() { - super({ - id: OpenDocumentationUrlAction.ID, - title: { value: localize('openDocumentationUrl', "Documentation"), original: 'Documentation' }, - category: CATEGORIES.Help, - f1: true - }); - } - - run(accessor: ServicesAccessor): void { - const productService = accessor.get(IProductService); - const openerService = accessor.get(IOpenerService); - - if (productService.documentationUrl) { - openerService.open(URI.parse(productService.documentationUrl)); - } - } -} - class OpenIntroductoryVideosUrlAction extends Action2 { static readonly ID = 'workbench.action.openIntroductoryVideosUrl'; @@ -78,9 +63,18 @@ class OpenIntroductoryVideosUrlAction extends Action2 { constructor() { super({ id: OpenIntroductoryVideosUrlAction.ID, - title: { value: localize('openIntroductoryVideosUrl', "Introductory Videos"), original: 'Introductory Videos' }, + title: { + value: localize('openIntroductoryVideosUrl', "Introductory Videos"), + mnemonicTitle: localize({ key: 'miIntroductoryVideos', comment: ['&& denotes a mnemonic'] }, "Introductory &&Videos"), + original: 'Introductory Videos' + }, category: CATEGORIES.Help, - f1: true + f1: true, + menu: { + id: MenuId.MenubarHelpMenu, + group: '2_reference', + order: 2 + } }); } @@ -102,9 +96,18 @@ class OpenTipsAndTricksUrlAction extends Action2 { constructor() { super({ id: OpenTipsAndTricksUrlAction.ID, - title: { value: localize('openTipsAndTricksUrl', "Tips and Tricks"), original: 'Tips and Tricks' }, + title: { + value: localize('openTipsAndTricksUrl', "Tips and Tricks"), + mnemonicTitle: localize({ key: 'miTipsAndTricks', comment: ['&& denotes a mnemonic'] }, "Tips and Tri&&cks"), + original: 'Tips and Tricks' + }, category: CATEGORIES.Help, - f1: true + f1: true, + menu: { + id: MenuId.MenubarHelpMenu, + group: '2_reference', + order: 3 + } }); } @@ -118,6 +121,39 @@ class OpenTipsAndTricksUrlAction extends Action2 { } } +class OpenDocumentationUrlAction extends Action2 { + + static readonly ID = 'workbench.action.openDocumentationUrl'; + static readonly AVAILABLE = !!product.documentationUrl; + + constructor() { + super({ + id: OpenDocumentationUrlAction.ID, + title: { + value: localize('openDocumentationUrl', "Documentation"), + mnemonicTitle: localize({ key: 'miDocumentation', comment: ['&& denotes a mnemonic'] }, "&&Documentation"), + original: 'Documentation' + }, + category: CATEGORIES.Help, + f1: true, + menu: { + id: MenuId.MenubarHelpMenu, + group: '1_welcome', + order: 3 + } + }); + } + + run(accessor: ServicesAccessor): void { + const productService = accessor.get(IProductService); + const openerService = accessor.get(IOpenerService); + + if (productService.documentationUrl) { + openerService.open(URI.parse(productService.documentationUrl)); + } + } +} + class OpenNewsletterSignupUrlAction extends Action2 { static readonly ID = 'workbench.action.openNewsletterSignupUrl'; @@ -151,9 +187,18 @@ class OpenTwitterUrlAction extends Action2 { constructor() { super({ id: OpenTwitterUrlAction.ID, - title: { value: localize('openTwitterUrl', "Join Us on Twitter"), original: 'Join Us on Twitter' }, + title: { + value: localize('openTwitterUrl', "Join Us on Twitter"), + mnemonicTitle: localize({ key: 'miTwitter', comment: ['&& denotes a mnemonic'] }, "&&Join Us on Twitter"), + original: 'Join Us on Twitter' + }, category: CATEGORIES.Help, - f1: true + f1: true, + menu: { + id: MenuId.MenubarHelpMenu, + group: '3_feedback', + order: 1 + } }); } @@ -175,9 +220,18 @@ class OpenRequestFeatureUrlAction extends Action2 { constructor() { super({ id: OpenRequestFeatureUrlAction.ID, - title: { value: localize('openUserVoiceUrl', "Search Feature Requests"), original: 'Search Feature Requests' }, + title: { + value: localize('openUserVoiceUrl', "Search Feature Requests"), + mnemonicTitle: localize({ key: 'miUserVoice', comment: ['&& denotes a mnemonic'] }, "&&Search Feature Requests"), + original: 'Search Feature Requests' + }, category: CATEGORIES.Help, - f1: true + f1: true, + menu: { + id: MenuId.MenubarHelpMenu, + group: '3_feedback', + order: 2 + } }); } @@ -199,9 +253,18 @@ class OpenLicenseUrlAction extends Action2 { constructor() { super({ id: OpenLicenseUrlAction.ID, - title: { value: localize('openLicenseUrl', "View License"), original: 'View License' }, + title: { + value: localize('openLicenseUrl', "View License"), + mnemonicTitle: localize({ key: 'miLicense', comment: ['&& denotes a mnemonic'] }, "View &&License"), + original: 'View License' + }, category: CATEGORIES.Help, - f1: true + f1: true, + menu: { + id: MenuId.MenubarHelpMenu, + group: '4_legal', + order: 1 + } }); } @@ -228,9 +291,18 @@ class OpenPrivacyStatementUrlAction extends Action2 { constructor() { super({ id: OpenPrivacyStatementUrlAction.ID, - title: { value: localize('openPrivacyStatement', "Privacy Statement"), original: 'Privacy Statement' }, + title: { + value: localize('openPrivacyStatement', "Privacy Statement"), + mnemonicTitle: localize({ key: 'miPrivacyStatement', comment: ['&& denotes a mnemonic'] }, "Privac&&y Statement"), + original: 'Privacy Statement' + }, category: CATEGORIES.Help, - f1: true + f1: true, + menu: { + id: MenuId.MenubarHelpMenu, + group: '4_legal', + order: 2 + } }); } @@ -255,10 +327,6 @@ if (KeybindingsReferenceAction.AVAILABLE) { registerAction2(KeybindingsReferenceAction); } -if (OpenDocumentationUrlAction.AVAILABLE) { - registerAction2(OpenDocumentationUrlAction); -} - if (OpenIntroductoryVideosUrlAction.AVAILABLE) { registerAction2(OpenIntroductoryVideosUrlAction); } @@ -267,6 +335,10 @@ if (OpenTipsAndTricksUrlAction.AVAILABLE) { registerAction2(OpenTipsAndTricksUrlAction); } +if (OpenDocumentationUrlAction.AVAILABLE) { + registerAction2(OpenDocumentationUrlAction); +} + if (OpenNewsletterSignupUrlAction.AVAILABLE) { registerAction2(OpenNewsletterSignupUrlAction); } @@ -286,98 +358,3 @@ if (OpenLicenseUrlAction.AVAILABLE) { if (OpenPrivacyStatementUrlAction.AVAILABE) { registerAction2(OpenPrivacyStatementUrlAction); } - -// --- Menu Registration - -// Help - -if (OpenDocumentationUrlAction.AVAILABLE) { - MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: '1_welcome', - command: { - id: OpenDocumentationUrlAction.ID, - title: localize({ key: 'miDocumentation', comment: ['&& denotes a mnemonic'] }, "&&Documentation") - }, - order: 3 - }); -} - -// Reference -if (KeybindingsReferenceAction.AVAILABLE) { - MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: '2_reference', - command: { - id: KeybindingsReferenceAction.ID, - title: localize({ key: 'miKeyboardShortcuts', comment: ['&& denotes a mnemonic'] }, "&&Keyboard Shortcuts Reference") - }, - order: 1 - }); -} - -if (OpenIntroductoryVideosUrlAction.AVAILABLE) { - MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: '2_reference', - command: { - id: OpenIntroductoryVideosUrlAction.ID, - title: localize({ key: 'miIntroductoryVideos', comment: ['&& denotes a mnemonic'] }, "Introductory &&Videos") - }, - order: 2 - }); -} - -if (OpenTipsAndTricksUrlAction.AVAILABLE) { - MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: '2_reference', - command: { - id: OpenTipsAndTricksUrlAction.ID, - title: localize({ key: 'miTipsAndTricks', comment: ['&& denotes a mnemonic'] }, "Tips and Tri&&cks") - }, - order: 3 - }); -} - -// Feedback -if (OpenTwitterUrlAction.AVAILABLE) { - MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: '3_feedback', - command: { - id: OpenTwitterUrlAction.ID, - title: localize({ key: 'miTwitter', comment: ['&& denotes a mnemonic'] }, "&&Join Us on Twitter") - }, - order: 1 - }); -} - -if (OpenRequestFeatureUrlAction.AVAILABLE) { - MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: '3_feedback', - command: { - id: OpenRequestFeatureUrlAction.ID, - title: localize({ key: 'miUserVoice', comment: ['&& denotes a mnemonic'] }, "&&Search Feature Requests") - }, - order: 2 - }); -} - -// Legal -if (OpenLicenseUrlAction.AVAILABLE) { - MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: '4_legal', - command: { - id: OpenLicenseUrlAction.ID, - title: localize({ key: 'miLicense', comment: ['&& denotes a mnemonic'] }, "View &&License") - }, - order: 1 - }); -} - -if (OpenPrivacyStatementUrlAction.AVAILABE) { - MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: '4_legal', - command: { - id: OpenPrivacyStatementUrlAction.ID, - title: localize({ key: 'miPrivacyStatement', comment: ['&& denotes a mnemonic'] }, "Privac&&y Statement") - }, - order: 2 - }); -} diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index a166be0cab..5fd4f58bf4 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -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 Severity from 'vs/base/common/severity'; +import { MenuId, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { CATEGORIES } from 'vs/workbench/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -20,15 +19,13 @@ import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/commo import { SideBarVisibleContext } from 'vs/workbench/common/viewlet'; 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 { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IActivityBarService } from 'vs/workbench/services/activityBar/browser/activityBarService'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -const registry = Registry.as(WorkbenchExtensions.WorkbenchActions); - // --- Close Side Bar -class CloseSidebarAction extends Action2 { +registerAction2(class extends Action2 { constructor() { super({ @@ -42,25 +39,32 @@ class CloseSidebarAction extends Action2 { run(accessor: ServicesAccessor): void { accessor.get(IWorkbenchLayoutService).setSideBarHidden(true); } -} - -registerAction2(CloseSidebarAction); +}); // --- Toggle Activity Bar export class ToggleActivityBarVisibilityAction extends Action2 { static readonly ID = 'workbench.action.toggleActivityBarVisibility'; - static readonly LABEL = localize('toggleActivityBar', "Toggle Activity Bar Visibility"); private static readonly activityBarVisibleKey = 'workbench.activityBar.visible'; constructor() { super({ id: ToggleActivityBarVisibilityAction.ID, - title: { value: ToggleActivityBarVisibilityAction.LABEL, original: 'Toggle Activity Bar Visibility' }, + title: { + value: localize('toggleActivityBar', "Toggle Activity Bar Visibility"), + mnemonicTitle: localize({ key: 'miShowActivityBar', comment: ['&& denotes a mnemonic'] }, "Show &&Activity Bar"), + original: 'Toggle Activity Bar Visibility' + }, category: CATEGORIES.View, - f1: true + f1: true, + toggled: ContextKeyExpr.equals('config.workbench.activityBar.visible', true), + menu: { + id: MenuId.MenubarAppearanceMenu, + group: '2_workbench_layout', + order: 4 + } }); } @@ -77,28 +81,26 @@ export class ToggleActivityBarVisibilityAction extends Action2 { registerAction2(ToggleActivityBarVisibilityAction); -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '2_workbench_layout', - command: { - id: ToggleActivityBarVisibilityAction.ID, - title: localize({ key: 'miShowActivityBar', comment: ['&& denotes a mnemonic'] }, "Show &&Activity Bar"), - toggled: ContextKeyExpr.equals('config.workbench.activityBar.visible', true) - }, - order: 4 -}); - // --- Toggle Centered Layout -class ToggleCenteredLayout extends Action2 { - - static readonly ID = 'workbench.action.toggleCenteredLayout'; +registerAction2(class extends Action2 { constructor() { super({ - id: ToggleCenteredLayout.ID, - title: { value: localize('toggleCenteredLayout', "Toggle Centered Layout"), original: 'Toggle Centered Layout' }, + id: 'workbench.action.toggleCenteredLayout', + title: { + value: localize('toggleCenteredLayout', "Toggle Centered Layout"), + mnemonicTitle: localize({ key: 'miToggleCenteredLayout', comment: ['&& denotes a mnemonic'] }, "&&Centered Layout"), + original: 'Toggle Centered Layout' + }, category: CATEGORIES.View, - f1: true + f1: true, + toggled: IsCenteredLayoutContext, + menu: { + id: MenuId.MenubarAppearanceMenu, + group: '1_toggle_view', + order: 3 + } }); } @@ -107,51 +109,21 @@ class ToggleCenteredLayout extends Action2 { layoutService.centerEditorLayout(!layoutService.isEditorLayoutCentered()); } -} - -registerAction2(ToggleCenteredLayout); - -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '1_toggle_view', - command: { - id: ToggleCenteredLayout.ID, - title: localize({ key: 'miToggleCenteredLayout', comment: ['&& denotes a mnemonic'] }, "&&Centered Layout"), - toggled: IsCenteredLayoutContext - }, - order: 3 }); // --- Toggle Sidebar Position -export class ToggleSidebarPositionAction extends Action { +export class ToggleSidebarPositionAction extends Action2 { static readonly ID = 'workbench.action.toggleSidebarPosition'; static readonly LABEL = localize('toggleSidebarPosition', "Toggle Side Bar Position"); private static readonly sidebarPositionConfigurationKey = 'workbench.sideBar.location'; - constructor( - id: string, - label: string, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super(id, label); - } - - override run(): Promise { - const position = this.layoutService.getSideBarPosition(); - const newPositionValue = (position === Position.LEFT) ? 'right' : 'left'; - - return this.configurationService.updateValue(ToggleSidebarPositionAction.sidebarPositionConfigurationKey, newPositionValue); - } - static getLabel(layoutService: IWorkbenchLayoutService): string { return layoutService.getSideBarPosition() === Position.LEFT ? localize('moveSidebarRight', "Move Side Bar Right") : localize('moveSidebarLeft', "Move Side Bar Left"); } -} -registerAction2(class extends Action2 { constructor() { super({ id: ToggleSidebarPositionAction.ID, @@ -160,10 +132,20 @@ registerAction2(class extends Action2 { f1: true }); } - run(accessor: ServicesAccessor) { - accessor.get(IInstantiationService).createInstance(ToggleSidebarPositionAction, ToggleSidebarPositionAction.ID, ToggleSidebarPositionAction.LABEL).run(); + + run(accessor: ServicesAccessor): Promise { + const layoutService = accessor.get(IWorkbenchLayoutService); + const configurationService = accessor.get(IConfigurationService); + + const position = layoutService.getSideBarPosition(); + const newPositionValue = (position === Position.LEFT) ? 'right' : 'left'; + + return configurationService.updateValue(ToggleSidebarPositionAction.sidebarPositionConfigurationKey, newPositionValue); } -}); +} + +registerAction2(ToggleSidebarPositionAction); + MenuRegistry.appendMenuItems([{ id: MenuId.ViewContainerTitleContext, item: { @@ -230,35 +212,32 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { order: 2 }); -// --- Toggle Sidebar Visibility +// --- Toggle Editor Visibility -export class ToggleEditorVisibilityAction extends Action { - static readonly ID = 'workbench.action.toggleEditorVisibility'; - static readonly LABEL = localize('toggleEditor', "Toggle Editor Area Visibility"); +registerAction2(class extends Action2 { - constructor( - id: string, - label: string, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.toggleEditorVisibility', + title: { + value: localize('toggleEditor', "Toggle Editor Area Visibility"), + mnemonicTitle: localize({ key: 'miShowEditorArea', comment: ['&& denotes a mnemonic'] }, "Show &&Editor Area"), + original: 'Toggle Editor Area Visibility' + }, + category: CATEGORIES.View, + f1: true, + toggled: EditorAreaVisibleContext, + menu: { + id: MenuId.MenubarAppearanceMenu, + group: '2_workbench_layout', + order: 5 + } + }); } - override async run(): Promise { - this.layoutService.toggleMaximizedPanel(); + run(accessor: ServicesAccessor): void { + accessor.get(IWorkbenchLayoutService).toggleMaximizedPanel(); } -} - -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleEditorVisibilityAction), 'View: Toggle Editor Area Visibility', CATEGORIES.View.value); - -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '2_workbench_layout', - command: { - id: ToggleEditorVisibilityAction.ID, - title: localize({ key: 'miShowEditorArea', comment: ['&& denotes a mnemonic'] }, "Show &&Editor Area"), - toggled: EditorAreaVisibleContext - }, - order: 5 }); MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { @@ -268,11 +247,15 @@ MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { order: 1 }); -export const TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID = 'workbench.action.toggleSidebarVisibility'; -registerAction2(class extends Action2 { +// Toggle Sidebar Visibility + +class ToggleSidebarVisibilityAction extends Action2 { + + static readonly ID = 'workbench.action.toggleSidebarVisibility'; + constructor() { super({ - id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID, + id: ToggleSidebarVisibilityAction.ID, title: { value: localize('toggleSidebar', "Toggle Side Bar Visibility"), original: 'Toggle Side Bar Visibility' }, category: CATEGORIES.View, f1: true, @@ -282,17 +265,22 @@ registerAction2(class extends Action2 { } }); } - run(accessor: ServicesAccessor) { + + run(accessor: ServicesAccessor): void { const layoutService = accessor.get(IWorkbenchLayoutService); + layoutService.setSideBarHidden(layoutService.isVisible(Parts.SIDEBAR_PART)); } -}); +} + +registerAction2(ToggleSidebarVisibilityAction); + MenuRegistry.appendMenuItems([{ id: MenuId.ViewContainerTitleContext, item: { group: '3_workbench_layout_move', command: { - id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID, + id: ToggleSidebarVisibilityAction.ID, title: localize('compositePart.hideSideBarLabel', "Hide Side Bar"), }, when: ContextKeyExpr.and(SideBarVisibleContext, ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), @@ -303,123 +291,127 @@ MenuRegistry.appendMenuItems([{ item: { group: '3_workbench_layout_move', command: { - id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID, + id: ToggleSidebarVisibilityAction.ID, title: localize('compositePart.hideSideBarLabel', "Hide Side Bar"), }, when: ContextKeyExpr.and(SideBarVisibleContext, ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), order: 2 } +}, { + id: MenuId.MenubarAppearanceMenu, + item: { + group: '2_workbench_layout', + command: { + id: ToggleSidebarVisibilityAction.ID, + title: localize({ key: 'miShowSidebar', comment: ['&& denotes a mnemonic'] }, "Show &&Side Bar"), + toggled: SideBarVisibleContext + }, + order: 1 + } }]); -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '2_workbench_layout', - command: { - id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID, - title: localize({ key: 'miShowSidebar', comment: ['&& denotes a mnemonic'] }, "Show &&Side Bar"), - toggled: SideBarVisibleContext - }, - order: 1 -}); - // --- Toggle Statusbar Visibility -export class ToggleStatusbarVisibilityAction extends Action { +export class ToggleStatusbarVisibilityAction extends Action2 { static readonly ID = 'workbench.action.toggleStatusbarVisibility'; - static readonly LABEL = localize('toggleStatusbar', "Toggle Status Bar Visibility"); private static readonly statusbarVisibleKey = 'workbench.statusBar.visible'; - constructor( - id: string, - label: string, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super(id, label); + constructor() { + super({ + id: ToggleStatusbarVisibilityAction.ID, + title: { + value: localize('toggleStatusbar', "Toggle Status Bar Visibility"), + mnemonicTitle: localize({ key: 'miShowStatusbar', comment: ['&& denotes a mnemonic'] }, "Show S&&tatus Bar"), + original: 'Toggle Status Bar Visibility' + }, + category: CATEGORIES.View, + f1: true, + toggled: ContextKeyExpr.equals('config.workbench.statusBar.visible', true), + menu: { + id: MenuId.MenubarAppearanceMenu, + group: '2_workbench_layout', + order: 3 + } + }); } - override run(): Promise { - const visibility = this.layoutService.isVisible(Parts.STATUSBAR_PART); + run(accessor: ServicesAccessor): Promise { + const layoutService = accessor.get(IWorkbenchLayoutService); + const configurationService = accessor.get(IConfigurationService); + + const visibility = layoutService.isVisible(Parts.STATUSBAR_PART); const newVisibilityValue = !visibility; - return this.configurationService.updateValue(ToggleStatusbarVisibilityAction.statusbarVisibleKey, newVisibilityValue); + return configurationService.updateValue(ToggleStatusbarVisibilityAction.statusbarVisibleKey, newVisibilityValue); } } -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleStatusbarVisibilityAction), 'View: Toggle Status Bar Visibility', CATEGORIES.View.value); - -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '2_workbench_layout', - command: { - id: ToggleStatusbarVisibilityAction.ID, - title: localize({ key: 'miShowStatusbar', comment: ['&& denotes a mnemonic'] }, "Show S&&tatus Bar"), - toggled: ContextKeyExpr.equals('config.workbench.statusBar.visible', true) - }, - order: 3 -}); +registerAction2(ToggleStatusbarVisibilityAction); // --- Toggle Tabs Visibility -class ToggleTabsVisibilityAction extends Action { +registerAction2(class extends Action2 { - static readonly ID = 'workbench.action.toggleTabsVisibility'; - static readonly LABEL = localize('toggleTabs', "Toggle Tab Visibility"); - - private static readonly tabsVisibleKey = 'workbench.editor.showTabs'; - - constructor( - id: string, - label: string, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.toggleTabsVisibility', + title: { + value: localize('toggleTabs', "Toggle Tab Visibility"), + original: 'Toggle Tab Visibility' + }, + category: CATEGORIES.View, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: undefined, + mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_W, }, + linux: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_W, } + } + }); } - override run(): Promise { - const visibility = this.configurationService.getValue(ToggleTabsVisibilityAction.tabsVisibleKey); + run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + + const visibility = configurationService.getValue('workbench.editor.showTabs'); const newVisibilityValue = !visibility; - return this.configurationService.updateValue(ToggleTabsVisibilityAction.tabsVisibleKey, newVisibilityValue); + return configurationService.updateValue('workbench.editor.showTabs', newVisibilityValue); } -} - -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleTabsVisibilityAction, { - primary: undefined, - mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_W, }, - linux: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_W, } -}), 'View: Toggle Tab Visibility', CATEGORIES.View.value); +}); // --- Toggle Zen Mode -class ToggleZenMode extends Action { +registerAction2(class extends Action2 { - static readonly ID = 'workbench.action.toggleZenMode'; - static readonly LABEL = localize('toggleZenMode', "Toggle Zen Mode"); - - constructor( - id: string, - label: string, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.toggleZenMode', + title: { + value: localize('toggleZenMode', "Toggle Zen Mode"), + mnemonicTitle: localize('miToggleZenMode', "Zen Mode"), + original: 'Toggle Zen Mode' + }, + category: CATEGORIES.View, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_Z) + }, + toggled: InEditorZenModeContext, + menu: { + id: MenuId.MenubarAppearanceMenu, + group: '1_toggle_view', + order: 2 + } + }); } - override async run(): Promise { - this.layoutService.toggleZenMode(); + run(accessor: ServicesAccessor): void { + return accessor.get(IWorkbenchLayoutService).toggleZenMode(); } -} - -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleZenMode, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_Z) }), 'View: Toggle Zen Mode', CATEGORIES.View.value); - -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '1_toggle_view', - command: { - id: ToggleZenMode.ID, - title: localize('miToggleZenMode', "Zen Mode"), - toggled: InEditorZenModeContext - }, - order: 2 }); KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -435,85 +427,103 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ // --- Toggle Menu Bar -export class ToggleMenuBarAction extends Action { - - static readonly ID = 'workbench.action.toggleMenuBar'; - static readonly LABEL = localize('toggleMenuBar', "Toggle Menu Bar"); - - constructor( - id: string, - label: string, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService - ) { - super(id, label); - } - - override async run(): Promise { - this.layoutService.toggleMenuBar(); - } -} - if (isWindows || isLinux || isWeb) { - registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleMenuBarAction), 'View: Toggle Menu Bar', CATEGORIES.View.value); + registerAction2(class extends Action2 { + + constructor() { + super({ + id: 'workbench.action.toggleMenuBar', + title: { + value: localize('toggleMenuBar', "Toggle Menu Bar"), + mnemonicTitle: localize({ key: 'miShowMenuBar', comment: ['&& denotes a mnemonic'] }, "Show Menu &&Bar"), + original: 'Toggle Menu Bar' + }, + category: CATEGORIES.View, + f1: true, + toggled: ContextKeyExpr.and(IsMacNativeContext.toNegated(), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'hidden'), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'toggle'), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'compact')), + menu: { + id: MenuId.MenubarAppearanceMenu, + group: '2_workbench_layout', + order: 0 + } + }); + } + + run(accessor: ServicesAccessor): void { + return accessor.get(IWorkbenchLayoutService).toggleMenuBar(); + } + }); } -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '2_workbench_layout', - command: { - id: ToggleMenuBarAction.ID, - title: localize({ key: 'miShowMenuBar', comment: ['&& denotes a mnemonic'] }, "Show Menu &&Bar"), - toggled: ContextKeyExpr.and(IsMacNativeContext.toNegated(), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'hidden'), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'toggle'), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'compact')) - }, - when: IsMacNativeContext.toNegated(), - order: 0 +// --- Reset View Locations + +registerAction2(class extends Action2 { + + constructor() { + super({ + id: 'workbench.action.resetViewLocations', + title: { + value: localize('resetViewLocations', "Reset View Locations"), + original: 'Reset View Locations' + }, + category: CATEGORIES.View, + f1: true + }); + } + + run(accessor: ServicesAccessor): void { + return accessor.get(IViewDescriptorService).reset(); + } }); -// --- Reset View Positions +// --- Move View -export class ResetViewLocationsAction extends Action { - static readonly ID = 'workbench.action.resetViewLocations'; - static readonly LABEL = localize('resetViewLocations', "Reset View Locations"); +registerAction2(class extends Action2 { - constructor( - id: string, - label: string, - @IViewDescriptorService private viewDescriptorService: IViewDescriptorService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.moveView', + title: { + value: localize('moveView', "Move View"), + original: 'Move View' + }, + category: CATEGORIES.View, + f1: true + }); } - override async run(): Promise { - this.viewDescriptorService.reset(); - } -} + async run(accessor: ServicesAccessor): Promise { + const viewDescriptorService = accessor.get(IViewDescriptorService); + const instantiationService = accessor.get(IInstantiationService); + const quickInputService = accessor.get(IQuickInputService); + const contextKeyService = accessor.get(IContextKeyService); + const activityBarService = accessor.get(IActivityBarService); + const panelService = accessor.get(IPanelService); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ResetViewLocationsAction), 'View: Reset View Locations', CATEGORIES.View.value); + const focusedViewId = FocusedViewContext.getValue(contextKeyService); + let viewId: string; -// --- Move View with Command -export class MoveViewAction extends Action { - static readonly ID = 'workbench.action.moveView'; - static readonly LABEL = localize('moveView', "Move View"); + if (focusedViewId && viewDescriptorService.getViewDescriptorById(focusedViewId)?.canMoveView) { + viewId = focusedViewId; + } - constructor( - id: string, - label: string, - @IViewDescriptorService private viewDescriptorService: IViewDescriptorService, - @IInstantiationService private instantiationService: IInstantiationService, - @IQuickInputService private quickInputService: IQuickInputService, - @IContextKeyService private contextKeyService: IContextKeyService, - @IActivityBarService private activityBarService: IActivityBarService, - @IPanelService private panelService: IPanelService - ) { - super(id, label); + viewId = await this.getView(quickInputService, activityBarService, viewDescriptorService, panelService, viewId!); + + if (!viewId) { + return; + } + + const moveFocusedViewAction = new MoveFocusedViewAction(); + instantiationService.invokeFunction(accessor => moveFocusedViewAction.run(accessor, viewId)); } - private getViewItems(): Array { + private getViewItems(activityBarService: IActivityBarService, viewDescriptorService: IViewDescriptorService, panelService: IPanelService): Array { const results: Array = []; - const viewlets = this.activityBarService.getVisibleViewContainerIds(); + const viewlets = activityBarService.getVisibleViewContainerIds(); viewlets.forEach(viewletId => { - const container = this.viewDescriptorService.getViewContainerById(viewletId)!; - const containerModel = this.viewDescriptorService.getViewContainerModel(container); + const container = viewDescriptorService.getViewContainerById(viewletId)!; + const containerModel = viewDescriptorService.getViewContainerModel(container); let hasAddedView = false; containerModel.visibleViewDescriptors.forEach(viewDescriptor => { @@ -534,10 +544,10 @@ export class MoveViewAction extends Action { }); }); - const panels = this.panelService.getPinnedPanels(); + const panels = panelService.getPinnedPanels(); panels.forEach(panel => { - const container = this.viewDescriptorService.getViewContainerById(panel.id)!; - const containerModel = this.viewDescriptorService.getViewContainerModel(container); + const container = viewDescriptorService.getViewContainerById(panel.id)!; + const containerModel = viewDescriptorService.getViewContainerModel(container); let hasAddedView = false; containerModel.visibleViewDescriptors.forEach(viewDescriptor => { @@ -561,10 +571,10 @@ export class MoveViewAction extends Action { return results; } - private async getView(viewId?: string): Promise { - const quickPick = this.quickInputService.createQuickPick(); + private async getView(quickInputService: IQuickInputService, activityBarService: IActivityBarService, viewDescriptorService: IViewDescriptorService, panelService: IPanelService, viewId?: string): Promise { + const quickPick = quickInputService.createQuickPick(); quickPick.placeholder = localize('moveFocusedView.selectView', "Select a View to Move"); - quickPick.items = this.getViewItems(); + quickPick.items = this.getViewItems(activityBarService, viewDescriptorService, panelService); quickPick.selectedItems = quickPick.items.filter(item => (item as IQuickPickItem).id === viewId) as IQuickPickItem[]; return new Promise((resolve, reject) => { @@ -584,68 +594,55 @@ export class MoveViewAction extends Action { quickPick.show(); }); } +}); - override async run(): Promise { - const focusedViewId = FocusedViewContext.getValue(this.contextKeyService); - let viewId: string; +// --- Move Focused View - if (focusedViewId && this.viewDescriptorService.getViewDescriptorById(focusedViewId)?.canMoveView) { - viewId = focusedViewId; - } +class MoveFocusedViewAction extends Action2 { - viewId = await this.getView(viewId!); - - if (!viewId) { - return; - } - - this.instantiationService.createInstance(MoveFocusedViewAction, MoveFocusedViewAction.ID, MoveFocusedViewAction.LABEL).run(viewId); - } -} - -registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveViewAction), 'View: Move View', CATEGORIES.View.value); - -// --- Move Focused View with Command -export class MoveFocusedViewAction extends Action { - static readonly ID = 'workbench.action.moveFocusedView'; - static readonly LABEL = localize('moveFocusedView', "Move Focused View"); - - constructor( - id: string, - label: string, - @IViewDescriptorService private viewDescriptorService: IViewDescriptorService, - @IViewsService private viewsService: IViewsService, - @IQuickInputService private quickInputService: IQuickInputService, - @IContextKeyService private contextKeyService: IContextKeyService, - @INotificationService private notificationService: INotificationService, - @IActivityBarService private activityBarService: IActivityBarService, - @IPanelService private panelService: IPanelService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.moveFocusedView', + title: { + value: localize('moveFocusedView', "Move Focused View"), + original: 'Move Focused View' + }, + category: CATEGORIES.View, + precondition: FocusedViewContext.notEqualsTo(''), + f1: true + }); } - override async run(viewId: string): Promise { - const focusedViewId = viewId || FocusedViewContext.getValue(this.contextKeyService); + run(accessor: ServicesAccessor, viewId?: string): void { + const viewDescriptorService = accessor.get(IViewDescriptorService); + const viewsService = accessor.get(IViewsService); + const quickInputService = accessor.get(IQuickInputService); + const contextKeyService = accessor.get(IContextKeyService); + const dialogService = accessor.get(IDialogService); + const activityBarService = accessor.get(IActivityBarService); + const panelService = accessor.get(IPanelService); + + const focusedViewId = viewId || FocusedViewContext.getValue(contextKeyService); if (focusedViewId === undefined || focusedViewId.trim() === '') { - this.notificationService.error(localize('moveFocusedView.error.noFocusedView', "There is no view currently focused.")); + dialogService.show(Severity.Error, localize('moveFocusedView.error.noFocusedView', "There is no view currently focused.")); return; } - const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(focusedViewId); + const viewDescriptor = viewDescriptorService.getViewDescriptorById(focusedViewId); if (!viewDescriptor || !viewDescriptor.canMoveView) { - this.notificationService.error(localize('moveFocusedView.error.nonMovableView', "The currently focused view is not movable.")); + dialogService.show(Severity.Error, localize('moveFocusedView.error.nonMovableView', "The currently focused view is not movable.")); return; } - const quickPick = this.quickInputService.createQuickPick(); + const quickPick = quickInputService.createQuickPick(); quickPick.placeholder = localize('moveFocusedView.selectDestination', "Select a Destination for the View"); quickPick.title = localize({ key: 'moveFocusedView.title', comment: ['{0} indicates the title of the view the user has selected to move.'] }, "View: Move {0}", viewDescriptor.name); const items: Array = []; - const currentContainer = this.viewDescriptorService.getViewContainerByViewId(focusedViewId)!; - const currentLocation = this.viewDescriptorService.getViewLocationById(focusedViewId)!; - const isViewSolo = this.viewDescriptorService.getViewContainerModel(currentContainer).allViewDescriptors.length === 1; + const currentContainer = viewDescriptorService.getViewContainerByViewId(focusedViewId)!; + const currentLocation = viewDescriptorService.getViewLocationById(focusedViewId)!; + const isViewSolo = viewDescriptorService.getViewContainerModel(currentContainer).allViewDescriptors.length === 1; if (!(isViewSolo && currentLocation === ViewContainerLocation.Panel)) { items.push({ @@ -666,19 +663,19 @@ export class MoveFocusedViewAction extends Action { label: localize('sidebar', "Side Bar") }); - const pinnedViewlets = this.activityBarService.getVisibleViewContainerIds(); + const pinnedViewlets = activityBarService.getVisibleViewContainerIds(); items.push(...pinnedViewlets .filter(viewletId => { - if (viewletId === this.viewDescriptorService.getViewContainerByViewId(focusedViewId)!.id) { + if (viewletId === viewDescriptorService.getViewContainerByViewId(focusedViewId)!.id) { return false; } - return !this.viewDescriptorService.getViewContainerById(viewletId)!.rejectAddedViews; + return !viewDescriptorService.getViewContainerById(viewletId)!.rejectAddedViews; }) .map(viewletId => { return { id: viewletId, - label: this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerById(viewletId)!)!.title + label: viewDescriptorService.getViewContainerModel(viewDescriptorService.getViewContainerById(viewletId)!)!.title }; })); @@ -687,19 +684,19 @@ export class MoveFocusedViewAction extends Action { label: localize('panel', "Panel") }); - const pinnedPanels = this.panelService.getPinnedPanels(); + const pinnedPanels = panelService.getPinnedPanels(); items.push(...pinnedPanels .filter(panel => { - if (panel.id === this.viewDescriptorService.getViewContainerByViewId(focusedViewId)!.id) { + if (panel.id === viewDescriptorService.getViewContainerByViewId(focusedViewId)!.id) { return false; } - return !this.viewDescriptorService.getViewContainerById(panel.id)!.rejectAddedViews; + return !viewDescriptorService.getViewContainerById(panel.id)!.rejectAddedViews; }) .map(panel => { return { id: panel.id, - label: this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerById(panel.id)!)!.title + label: viewDescriptorService.getViewContainerModel(viewDescriptorService.getViewContainerById(panel.id)!)!.title }; })); @@ -709,14 +706,14 @@ export class MoveFocusedViewAction extends Action { const destination = quickPick.selectedItems[0]; if (destination.id === '_.panel.newcontainer') { - this.viewDescriptorService.moveViewToLocation(viewDescriptor!, ViewContainerLocation.Panel); - this.viewsService.openView(focusedViewId, true); + viewDescriptorService.moveViewToLocation(viewDescriptor!, ViewContainerLocation.Panel); + viewsService.openView(focusedViewId, true); } else if (destination.id === '_.sidebar.newcontainer') { - this.viewDescriptorService.moveViewToLocation(viewDescriptor!, ViewContainerLocation.Sidebar); - this.viewsService.openView(focusedViewId, true); + viewDescriptorService.moveViewToLocation(viewDescriptor!, ViewContainerLocation.Sidebar); + viewsService.openView(focusedViewId, true); } else if (destination.id) { - this.viewDescriptorService.moveViewsToContainer([viewDescriptor], this.viewDescriptorService.getViewContainerById(destination.id)!); - this.viewsService.openView(focusedViewId, true); + viewDescriptorService.moveViewsToContainer([viewDescriptor], viewDescriptorService.getViewContainerById(destination.id)!); + viewsService.openView(focusedViewId, true); } quickPick.hide(); @@ -726,54 +723,56 @@ export class MoveFocusedViewAction extends Action { } } -registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveFocusedViewAction), 'View: Move Focused View', CATEGORIES.View.value, FocusedViewContext.notEqualsTo('')); +registerAction2(MoveFocusedViewAction); -// --- Reset View Location with Command -export class ResetFocusedViewLocationAction extends Action { - static readonly ID = 'workbench.action.resetFocusedViewLocation'; - static readonly LABEL = localize('resetFocusedViewLocation', "Reset Focused View Location"); +// --- Reset Focused View Location - constructor( - id: string, - label: string, - @IViewDescriptorService private viewDescriptorService: IViewDescriptorService, - @IContextKeyService private contextKeyService: IContextKeyService, - @INotificationService private notificationService: INotificationService, - @IViewsService private viewsService: IViewsService - ) { - super(id, label); +registerAction2(class extends Action2 { + + constructor() { + super({ + id: 'workbench.action.resetFocusedViewLocation', + title: { + value: localize('resetFocusedViewLocation', "Reset Focused View Location"), + original: 'Reset Focused View Location' + }, + category: CATEGORIES.View, + f1: true, + precondition: FocusedViewContext.notEqualsTo('') + }); } - override async run(): Promise { - const focusedViewId = FocusedViewContext.getValue(this.contextKeyService); + run(accessor: ServicesAccessor): void { + const viewDescriptorService = accessor.get(IViewDescriptorService); + const contextKeyService = accessor.get(IContextKeyService); + const dialogService = accessor.get(IDialogService); + const viewsService = accessor.get(IViewsService); + + const focusedViewId = FocusedViewContext.getValue(contextKeyService); let viewDescriptor: IViewDescriptor | null = null; if (focusedViewId !== undefined && focusedViewId.trim() !== '') { - viewDescriptor = this.viewDescriptorService.getViewDescriptorById(focusedViewId); + viewDescriptor = viewDescriptorService.getViewDescriptorById(focusedViewId); } if (!viewDescriptor) { - this.notificationService.error(localize('resetFocusedView.error.noFocusedView', "There is no view currently focused.")); + dialogService.show(Severity.Error, localize('resetFocusedView.error.noFocusedView', "There is no view currently focused.")); return; } - const defaultContainer = this.viewDescriptorService.getDefaultContainerById(viewDescriptor.id); - if (!defaultContainer || defaultContainer === this.viewDescriptorService.getViewContainerByViewId(viewDescriptor.id)) { + const defaultContainer = viewDescriptorService.getDefaultContainerById(viewDescriptor.id); + if (!defaultContainer || defaultContainer === viewDescriptorService.getViewContainerByViewId(viewDescriptor.id)) { return; } - this.viewDescriptorService.moveViewsToContainer([viewDescriptor], defaultContainer); - this.viewsService.openView(viewDescriptor.id, true); - + viewDescriptorService.moveViewsToContainer([viewDescriptor], defaultContainer); + viewsService.openView(viewDescriptor.id, true); } -} - -registry.registerWorkbenchAction(SyncActionDescriptor.from(ResetFocusedViewLocationAction), 'View: Reset Focused View Location', CATEGORIES.View.value, FocusedViewContext.notEqualsTo('')); - +}); // --- Resize View -export abstract class BaseResizeViewAction extends Action2 { +abstract class BaseResizeViewAction extends Action2 { protected static readonly RESIZE_INCREMENT = 6.5; // This is a media-size percentage @@ -802,7 +801,7 @@ export abstract class BaseResizeViewAction extends Action2 { } } -export class IncreaseViewSizeAction extends BaseResizeViewAction { +class IncreaseViewSizeAction extends BaseResizeViewAction { constructor() { super({ @@ -812,12 +811,12 @@ export class IncreaseViewSizeAction extends BaseResizeViewAction { }); } - async run(accessor: ServicesAccessor): Promise { + run(accessor: ServicesAccessor): void { this.resizePart(BaseResizeViewAction.RESIZE_INCREMENT, BaseResizeViewAction.RESIZE_INCREMENT, accessor.get(IWorkbenchLayoutService)); } } -export class IncreaseViewWidthAction extends BaseResizeViewAction { +class IncreaseViewWidthAction extends BaseResizeViewAction { constructor() { super({ @@ -827,12 +826,12 @@ export class IncreaseViewWidthAction extends BaseResizeViewAction { }); } - async run(accessor: ServicesAccessor): Promise { + run(accessor: ServicesAccessor): void { this.resizePart(BaseResizeViewAction.RESIZE_INCREMENT, 0, accessor.get(IWorkbenchLayoutService), Parts.EDITOR_PART); } } -export class IncreaseViewHeightAction extends BaseResizeViewAction { +class IncreaseViewHeightAction extends BaseResizeViewAction { constructor() { super({ @@ -841,12 +840,13 @@ export class IncreaseViewHeightAction extends BaseResizeViewAction { f1: true }); } - async run(accessor: ServicesAccessor): Promise { + + run(accessor: ServicesAccessor): void { this.resizePart(0, BaseResizeViewAction.RESIZE_INCREMENT, accessor.get(IWorkbenchLayoutService), Parts.EDITOR_PART); } } -export class DecreaseViewSizeAction extends BaseResizeViewAction { +class DecreaseViewSizeAction extends BaseResizeViewAction { constructor() { super({ @@ -856,12 +856,12 @@ export class DecreaseViewSizeAction extends BaseResizeViewAction { }); } - async run(accessor: ServicesAccessor): Promise { + run(accessor: ServicesAccessor): void { this.resizePart(-BaseResizeViewAction.RESIZE_INCREMENT, -BaseResizeViewAction.RESIZE_INCREMENT, accessor.get(IWorkbenchLayoutService)); } } -export class DecreaseViewWidthAction extends BaseResizeViewAction { +class DecreaseViewWidthAction extends BaseResizeViewAction { constructor() { super({ id: 'workbench.action.decreaseViewWidth', @@ -870,13 +870,12 @@ export class DecreaseViewWidthAction extends BaseResizeViewAction { }); } - async run(accessor: ServicesAccessor): Promise { + run(accessor: ServicesAccessor): void { this.resizePart(-BaseResizeViewAction.RESIZE_INCREMENT, 0, accessor.get(IWorkbenchLayoutService), Parts.EDITOR_PART); } } - -export class DecreaseViewHeightAction extends BaseResizeViewAction { +class DecreaseViewHeightAction extends BaseResizeViewAction { constructor() { super({ @@ -886,7 +885,7 @@ export class DecreaseViewHeightAction extends BaseResizeViewAction { }); } - async run(accessor: ServicesAccessor): Promise { + run(accessor: ServicesAccessor): void { this.resizePart(0, -BaseResizeViewAction.RESIZE_INCREMENT, accessor.get(IWorkbenchLayoutService), Parts.EDITOR_PART); } } diff --git a/src/vs/workbench/browser/actions/windowActions.ts b/src/vs/workbench/browser/actions/windowActions.ts index adbd061aa3..d90333ff22 100644 --- a/src/vs/workbench/browser/actions/windowActions.ts +++ b/src/vs/workbench/browser/actions/windowActions.ts @@ -4,15 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { Action } from 'vs/base/common/actions'; import { IWindowOpenable } from 'vs/platform/windows/common/windows'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { SyncActionDescriptor, MenuRegistry, MenuId, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; -import { Registry } from 'vs/platform/registry/common/platform'; +import { MenuRegistry, MenuId, Action2, registerAction2, IAction2Options } from 'vs/platform/actions/common/actions'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { IsFullscreenContext } from 'vs/workbench/browser/contextkeys'; -import { IsMacNativeContext, IsDevelopmentContext, IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; -import { IWorkbenchActionRegistry, Extensions, CATEGORIES } from 'vs/workbench/common/actions'; +import { IsMacNativeContext, IsDevelopmentContext, IsWebContext, IsIOSContext } from 'vs/platform/contextkey/common/contextkeys'; +import { CATEGORIES } from 'vs/workbench/common/actions'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputButton, IQuickInputService, IQuickPickSeparator, IKeyMods, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -34,6 +32,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { isHTMLElement } from 'vs/base/browser/dom'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; export const inRecentFilesPickerContextKey = 'inRecentFilesPicker'; @@ -42,7 +41,9 @@ interface IRecentlyOpenedPick extends IQuickPickItem { openable: IWindowOpenable; } -abstract class BaseOpenRecentAction extends Action { +const fileCategory = { value: localize('file', "File"), original: 'File' }; + +abstract class BaseOpenRecentAction extends Action2 { private readonly removeFromRecentlyOpened: IQuickInputButton = { iconClass: Codicon.removeClose.classNames, @@ -60,27 +61,25 @@ abstract class BaseOpenRecentAction extends Action { tooltip: localize('dirtyRecentlyOpenedWorkspace', "Workspace With Unsaved Files"), }; - constructor( - id: string, - label: string, - private workspacesService: IWorkspacesService, - private quickInputService: IQuickInputService, - private contextService: IWorkspaceContextService, - private labelService: ILabelService, - private keybindingService: IKeybindingService, - private modelService: IModelService, - private modeService: IModeService, - private hostService: IHostService, - private dialogService: IDialogService - ) { - super(id, label); + constructor(desc: Readonly) { + super(desc); } protected abstract isQuickNavigate(): boolean; - override async run(): Promise { - const recentlyOpened = await this.workspacesService.getRecentlyOpened(); - const dirtyWorkspacesAndFolders = await this.workspacesService.getDirtyWorkspaces(); + override async run(accessor: ServicesAccessor): Promise { + const workspacesService = accessor.get(IWorkspacesService); + const quickInputService = accessor.get(IQuickInputService); + const contextService = accessor.get(IWorkspaceContextService); + const labelService = accessor.get(ILabelService); + const keybindingService = accessor.get(IKeybindingService); + const modelService = accessor.get(IModelService); + const modeService = accessor.get(IModeService); + const hostService = accessor.get(IHostService); + const dialogService = accessor.get(IDialogService); + + const recentlyOpened = await workspacesService.getRecentlyOpened(); + const dirtyWorkspacesAndFolders = await workspacesService.getDirtyWorkspaces(); let hasWorkspaces = false; @@ -113,23 +112,23 @@ abstract class BaseOpenRecentAction extends Action { for (const recent of recentlyOpened.workspaces) { const isDirty = isRecentFolder(recent) ? dirtyFolders.has(recent.folderUri) : dirtyWorkspaces.has(recent.workspace.configPath); - workspacePicks.push(this.toQuickPick(recent, isDirty)); + workspacePicks.push(this.toQuickPick(modelService, modeService, labelService, recent, isDirty)); } // Fill any backup workspace that is not yet shown at the end for (const dirtyWorkspaceOrFolder of dirtyWorkspacesAndFolders) { if (URI.isUri(dirtyWorkspaceOrFolder) && !recentFolders.has(dirtyWorkspaceOrFolder)) { - workspacePicks.push(this.toQuickPick({ folderUri: dirtyWorkspaceOrFolder }, true)); + workspacePicks.push(this.toQuickPick(modelService, modeService, labelService, { folderUri: dirtyWorkspaceOrFolder }, true)); } else if (isWorkspaceIdentifier(dirtyWorkspaceOrFolder) && !recentWorkspaces.has(dirtyWorkspaceOrFolder.configPath)) { - workspacePicks.push(this.toQuickPick({ workspace: dirtyWorkspaceOrFolder }, true)); + workspacePicks.push(this.toQuickPick(modelService, modeService, labelService, { workspace: dirtyWorkspaceOrFolder }, true)); } } - const filePicks = recentlyOpened.files.map(p => this.toQuickPick(p, false)); + const filePicks = recentlyOpened.files.map(p => this.toQuickPick(modelService, modeService, labelService, p, false)); // focus second entry if the first recent workspace is the current workspace const firstEntry = recentlyOpened.workspaces[0]; - const autoFocusSecondEntry: boolean = firstEntry && this.contextService.isCurrentWorkspace(isRecentWorkspace(firstEntry) ? firstEntry.workspace : firstEntry.folderUri); + const autoFocusSecondEntry: boolean = firstEntry && contextService.isCurrentWorkspace(isRecentWorkspace(firstEntry) ? firstEntry.workspace : firstEntry.folderUri); let keyMods: IKeyMods | undefined; @@ -137,25 +136,25 @@ abstract class BaseOpenRecentAction extends Action { const fileSeparator: IQuickPickSeparator = { type: 'separator', label: localize('files', "files") }; const picks = [workspaceSeparator, ...workspacePicks, fileSeparator, ...filePicks]; - const pick = await this.quickInputService.pick(picks, { + const pick = await quickInputService.pick(picks, { contextKey: inRecentFilesPickerContextKey, activeItem: [...workspacePicks, ...filePicks][autoFocusSecondEntry ? 1 : 0], placeHolder: isMacintosh ? localize('openRecentPlaceholderMac', "Select to open (hold Cmd-key to force new window or Alt-key for same window)") : localize('openRecentPlaceholder', "Select to open (hold Ctrl-key to force new window or Alt-key for same window)"), matchOnDescription: true, onKeyMods: mods => keyMods = mods, - quickNavigate: this.isQuickNavigate() ? { keybindings: this.keybindingService.lookupKeybindings(this.id) } : undefined, + quickNavigate: this.isQuickNavigate() ? { keybindings: keybindingService.lookupKeybindings(this.desc.id) } : undefined, onDidTriggerItemButton: async context => { // Remove if (context.button === this.removeFromRecentlyOpened) { - await this.workspacesService.removeRecentlyOpened([context.item.resource]); + await workspacesService.removeRecentlyOpened([context.item.resource]); context.removeItem(); } // 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({ + const result = await dialogService.confirm({ type: 'question', title: isDirtyWorkspace ? localize('dirtyWorkspace', "Workspace with Unsaved Files") : localize('dirtyFolder', "Folder with Unsaved Files"), message: isDirtyWorkspace ? localize('dirtyWorkspaceConfirm', "Do you want to open the workspace to review the unsaved files?") : localize('dirtyFolderConfirm', "Do you want to open the folder to review the unsaved files?"), @@ -163,19 +162,19 @@ abstract class BaseOpenRecentAction extends Action { }); if (result.confirmed) { - this.hostService.openWindow([context.item.openable]); - this.quickInputService.cancel(); + hostService.openWindow([context.item.openable]); + quickInputService.cancel(); } } } }); if (pick) { - return this.hostService.openWindow([pick.openable], { forceNewWindow: keyMods?.ctrlCmd, forceReuseWindow: keyMods?.alt }); + return hostService.openWindow([pick.openable], { forceNewWindow: keyMods?.ctrlCmd, forceReuseWindow: keyMods?.alt }); } } - private toQuickPick(recent: IRecent, isDirty: boolean): IRecentlyOpenedPick { + private toQuickPick(modelService: IModelService, modeService: IModeService, labelService: ILabelService, recent: IRecent, isDirty: boolean): IRecentlyOpenedPick { let openable: IWindowOpenable | undefined; let iconClasses: string[]; let fullLabel: string | undefined; @@ -185,26 +184,26 @@ abstract class BaseOpenRecentAction extends Action { // Folder if (isRecentFolder(recent)) { resource = recent.folderUri; - iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.FOLDER); + iconClasses = getIconClasses(modelService, modeService, resource, FileKind.FOLDER); openable = { folderUri: resource }; - fullLabel = recent.label || this.labelService.getWorkspaceLabel(resource, { verbose: true }); + fullLabel = recent.label || labelService.getWorkspaceLabel(resource, { verbose: true }); } // Workspace else if (isRecentWorkspace(recent)) { resource = recent.workspace.configPath; - iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.ROOT_FOLDER); + iconClasses = getIconClasses(modelService, modeService, resource, FileKind.ROOT_FOLDER); openable = { workspaceUri: resource }; - fullLabel = recent.label || this.labelService.getWorkspaceLabel(recent.workspace, { verbose: true }); + fullLabel = recent.label || labelService.getWorkspaceLabel(recent.workspace, { verbose: true }); isWorkspace = true; } // File else { resource = recent.fileUri; - iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.FILE); + iconClasses = getIconClasses(modelService, modeService, resource, FileKind.FILE); openable = { fileUri: resource }; - fullLabel = recent.label || this.labelService.getUriLabel(resource); + fullLabel = recent.label || labelService.getUriLabel(resource); } const { name, parentPath } = splitName(fullLabel); @@ -223,23 +222,27 @@ abstract class BaseOpenRecentAction extends Action { export class OpenRecentAction extends BaseOpenRecentAction { - static readonly ID = 'workbench.action.openRecent'; - static readonly LABEL = localize('openRecent', "Open Recent..."); - - constructor( - id: string, - label: string, - @IWorkspacesService workspacesService: IWorkspacesService, - @IQuickInputService quickInputService: IQuickInputService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IKeybindingService keybindingService: IKeybindingService, - @IModelService modelService: IModelService, - @IModeService modeService: IModeService, - @ILabelService labelService: ILabelService, - @IHostService hostService: IHostService, - @IDialogService dialogService: IDialogService - ) { - super(id, label, workspacesService, quickInputService, contextService, labelService, keybindingService, modelService, modeService, hostService, dialogService); + constructor() { + super({ + id: 'workbench.action.openRecent', + title: { + value: localize('openRecent', "Open Recent..."), + mnemonicTitle: localize({ key: 'miMore', comment: ['&& denotes a mnemonic'] }, "&&More..."), + original: 'Open Recent...' + }, + category: fileCategory, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KEY_R, + mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_R } + }, + menu: { + id: MenuId.MenubarRecentMenu, + group: 'y_more', + order: 1 + } + }); } protected isQuickNavigate(): boolean { @@ -249,23 +252,13 @@ export class OpenRecentAction extends BaseOpenRecentAction { class QuickPickRecentAction extends BaseOpenRecentAction { - static readonly ID = 'workbench.action.quickOpenRecent'; - static readonly LABEL = localize('quickOpenRecent', "Quick Open Recent..."); - - constructor( - id: string, - label: string, - @IWorkspacesService workspacesService: IWorkspacesService, - @IQuickInputService quickInputService: IQuickInputService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IKeybindingService keybindingService: IKeybindingService, - @IModelService modelService: IModelService, - @IModeService modeService: IModeService, - @ILabelService labelService: ILabelService, - @IHostService hostService: IHostService, - @IDialogService dialogService: IDialogService - ) { - super(id, label, workspacesService, quickInputService, contextService, labelService, keybindingService, modelService, modeService, hostService, dialogService); + constructor() { + super({ + id: 'workbench.action.quickOpenRecent', + title: { value: localize('quickOpenRecent', "Quick Open Recent..."), original: 'Quick Open Recent...' }, + category: fileCategory, + f1: true + }); } protected isQuickNavigate(): boolean { @@ -273,75 +266,122 @@ class QuickPickRecentAction extends BaseOpenRecentAction { } } -class ToggleFullScreenAction extends Action { +class ToggleFullScreenAction extends Action2 { - static readonly ID = 'workbench.action.toggleFullScreen'; - static readonly LABEL = localize('toggleFullScreen', "Toggle Full Screen"); - - constructor( - id: string, - label: string, - @IHostService private readonly hostService: IHostService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.toggleFullScreen', + title: { + value: localize('toggleFullScreen', "Toggle Full Screen"), + mnemonicTitle: localize({ key: 'miToggleFullScreen', comment: ['&& denotes a mnemonic'] }, "&&Full Screen"), + original: 'Toggle Full Screen' + }, + category: CATEGORIES.View.value, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.F11, + mac: { + primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_F + } + }, + precondition: IsIOSContext.toNegated(), + toggled: IsFullscreenContext, + menu: { + id: MenuId.MenubarAppearanceMenu, + group: '1_toggle_view', + order: 1 + } + }); } - override run(): Promise { - return this.hostService.toggleFullScreen(); + override run(accessor: ServicesAccessor): Promise { + const hostService = accessor.get(IHostService); + + return hostService.toggleFullScreen(); } } -export class ReloadWindowAction extends Action { +export class ReloadWindowAction extends Action2 { static readonly ID = 'workbench.action.reloadWindow'; - static readonly LABEL = localize('reloadWindow', "Reload Window"); - constructor( - id: string, - label: string, - @IHostService private readonly hostService: IHostService - ) { - super(id, label); + constructor() { + super({ + id: ReloadWindowAction.ID, + title: { value: localize('reloadWindow', "Reload Window"), original: 'Reload Window' }, + category: CATEGORIES.Developer.value, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib + 50, + when: IsDevelopmentContext, + primary: KeyMod.CtrlCmd | KeyCode.KEY_R + } + }); } - override async run(): Promise { - await this.hostService.reload(); + override run(accessor: ServicesAccessor): Promise { + const hostService = accessor.get(IHostService); + + return hostService.reload(); } } -class ShowAboutDialogAction extends Action { +class ShowAboutDialogAction extends Action2 { - static readonly ID = 'workbench.action.showAboutDialog'; - static readonly LABEL = localize('about', "About"); - - constructor( - id: string, - label: string, - @IDialogService private readonly dialogService: IDialogService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.showAboutDialog', + title: { + value: localize('about', "About"), + mnemonicTitle: localize({ key: 'miAbout', comment: ['&& denotes a mnemonic'] }, "&&About"), + original: 'About' + }, + category: CATEGORIES.Help.value, + f1: true, + menu: { + id: MenuId.MenubarHelpMenu, + group: 'z_about', + order: 1, + when: IsMacNativeContext.toNegated() + } + }); } - override run(): Promise { - return this.dialogService.about(); + override run(accessor: ServicesAccessor): Promise { + const dialogService = accessor.get(IDialogService); + + return dialogService.about(); } } -export class NewWindowAction extends Action { +class NewWindowAction extends Action2 { - static readonly ID = 'workbench.action.newWindow'; - static readonly LABEL = localize('newWindow', "New Window"); - - constructor( - id: string, - label: string, - @IHostService private readonly hostService: IHostService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.newWindow', + title: { + value: localize('newWindow', "New Window"), + mnemonicTitle: localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window"), + original: 'New Window' + }, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_N + }, + menu: { + id: MenuId.MenubarFileMenu, + group: '1_new', + order: 2 + } + }); } - override run(): Promise { - return this.hostService.openWindow({ remoteAuthority: null }); + override run(accessor: ServicesAccessor): Promise { + const hostService = accessor.get(IHostService); + + return hostService.openWindow({ remoteAuthority: null }); } } @@ -350,7 +390,7 @@ class BlurAction extends Action2 { constructor() { super({ id: 'workbench.action.blur', - title: localize('blur', "Remove keyboard focus from focused element") + title: { value: localize('blur', "Remove keyboard focus from focused element"), original: 'Remove keyboard focus from focused element' } }); } @@ -363,21 +403,14 @@ class BlurAction extends Action2 { } } -const registry = Registry.as(Extensions.WorkbenchActions); - // --- Actions Registration -const fileCategory = localize('file', "File"); -registry.registerWorkbenchAction(SyncActionDescriptor.from(NewWindowAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_N }), 'New Window'); -registry.registerWorkbenchAction(SyncActionDescriptor.from(QuickPickRecentAction), 'File: Quick Open Recent...', fileCategory); -registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenRecentAction, { primary: KeyMod.CtrlCmd | KeyCode.KEY_R, mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_R } }), 'File: Open Recent...', fileCategory); - -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleFullScreenAction, { primary: KeyCode.F11, mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_F } }), 'View: Toggle Full Screen', CATEGORIES.View.value); - -registry.registerWorkbenchAction(SyncActionDescriptor.from(ReloadWindowAction), 'Developer: Reload Window', CATEGORIES.Developer.value, IsWebContext.toNegated()); - -registry.registerWorkbenchAction(SyncActionDescriptor.from(ShowAboutDialogAction), `Help: About`, CATEGORIES.Help.value); - +registerAction2(NewWindowAction); +registerAction2(ToggleFullScreenAction); +registerAction2(QuickPickRecentAction); +registerAction2(OpenRecentAction); +registerAction2(ReloadWindowAction); +registerAction2(ShowAboutDialogAction); registerAction2(BlurAction); // --- Commands/Keybindings Registration @@ -404,13 +437,6 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_R } }); -KeybindingsRegistry.registerKeybindingRule({ - id: ReloadWindowAction.ID, - weight: KeybindingWeight.WorkbenchContrib + 50, - when: IsDevelopmentContext, - primary: KeyMod.CtrlCmd | KeyCode.KEY_R -}); - CommandsRegistry.registerCommand('workbench.action.toggleConfirmBeforeClose', accessor => { const configurationService = accessor.get(IConfigurationService); const setting = configurationService.inspect<'always' | 'keyboardOnly' | 'never'>('window.confirmBeforeClose').userValue; @@ -431,47 +457,9 @@ MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { when: IsWebContext }); -MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: '1_new', - command: { - id: NewWindowAction.ID, - title: localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window") - }, - order: 2 -}); - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { title: localize({ key: 'miOpenRecent', comment: ['&& denotes a mnemonic'] }, "Open &&Recent"), submenu: MenuId.MenubarRecentMenu, group: '2_open', order: 4 }); - -MenuRegistry.appendMenuItem(MenuId.MenubarRecentMenu, { - group: 'y_more', - command: { - id: OpenRecentAction.ID, - title: localize({ key: 'miMore', comment: ['&& denotes a mnemonic'] }, "&&More...") - }, - order: 1 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '1_toggle_view', - command: { - id: ToggleFullScreenAction.ID, - title: localize({ key: 'miToggleFullScreen', comment: ['&& denotes a mnemonic'] }, "&&Full Screen"), - toggled: IsFullscreenContext - }, - order: 1 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: 'z_about', - command: { - id: ShowAboutDialogAction.ID, - title: localize({ key: 'miAbout', comment: ['&& denotes a mnemonic'] }, "&&About") - }, - order: 1, - when: IsMacNativeContext.toNegated() -}); diff --git a/src/vs/workbench/browser/actions/workspaceActions.ts b/src/vs/workbench/browser/actions/workspaceActions.ts index e3d4d43288..3c8bee1403 100644 --- a/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -11,20 +11,21 @@ import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/commo import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ADD_ROOT_FOLDER_COMMAND_ID, ADD_ROOT_FOLDER_LABEL, PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; -import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { MenuRegistry, MenuId, SyncActionDescriptor, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { MenuRegistry, MenuId, Action2, registerAction2, ILocalizedString } from 'vs/platform/actions/common/actions'; import { EmptyWorkspaceSupportContext, WorkbenchStateContext, WorkspaceFolderCountContext } from 'vs/workbench/browser/contextkeys'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import Severity from 'vs/base/common/severity'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWorkspacesService, hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; -import { WORKSPACE_TRUST_ENABLED } from 'vs/workbench/services/workspaces/common/workspaceTrust'; +import { WorkspaceTrustContext, WORKSPACE_TRUST_ENABLED } from 'vs/workbench/services/workspaces/common/workspaceTrust'; import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; + +const workspacesCategory: ILocalizedString = { value: localize('workspaces', "Workspaces"), original: 'Workspaces' }; export class OpenFileAction extends Action { @@ -98,29 +99,36 @@ export class OpenWorkspaceAction extends Action { } } -export class CloseWorkspaceAction extends Action { +export class CloseWorkspaceAction extends Action2 { static readonly ID = 'workbench.action.closeFolder'; - static readonly LABEL = localize('closeWorkspace', "Close Workspace"); - constructor( - id: string, - label: string, - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @INotificationService private readonly notificationService: INotificationService, - @IHostService private readonly hostService: IHostService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService - ) { - super(id, label); + constructor() { + super({ + id: CloseWorkspaceAction.ID, + title: { value: localize('closeWorkspace', "Close Workspace"), original: 'Close Workspace' }, + category: workspacesCategory, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: EmptyWorkspaceSupportContext, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_F) + } + }); } - override async run(): Promise { - if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { - this.notificationService.info(localize('noWorkspaceOrFolderOpened', "There is currently no workspace or folder opened in this instance to close.")); + override async run(accessor: ServicesAccessor): Promise { + const contextService = accessor.get(IWorkspaceContextService); + const dialogService = accessor.get(IDialogService); + const hostService = accessor.get(IHostService); + const environmentService = accessor.get(IWorkbenchEnvironmentService); + + if (contextService.getWorkbenchState() === WorkbenchState.EMPTY) { + dialogService.show(Severity.Error, localize('noWorkspaceOrFolderOpened', "There is currently no workspace or folder opened in this instance to close.")); return; } - return this.hostService.openWindow({ forceReuseWindow: true, remoteAuthority: this.environmentService.remoteAuthority }); + return hostService.openWindow({ forceReuseWindow: true, remoteAuthority: environmentService.remoteAuthority }); } } @@ -148,116 +156,120 @@ export class OpenWorkspaceConfigFileAction extends Action { } } -export class AddRootFolderAction extends Action { +export class AddRootFolderAction extends Action2 { static readonly ID = 'workbench.action.addRootFolder'; - static readonly LABEL = ADD_ROOT_FOLDER_LABEL; - constructor( - id: string, - label: string, - @ICommandService private readonly commandService: ICommandService - ) { - super(id, label); + constructor() { + super({ + id: AddRootFolderAction.ID, + title: ADD_ROOT_FOLDER_LABEL, + category: workspacesCategory, + f1: true + }); } - override run(): Promise { - return this.commandService.executeCommand(ADD_ROOT_FOLDER_COMMAND_ID); + override run(accessor: ServicesAccessor): Promise { + const commandService = accessor.get(ICommandService); + + return commandService.executeCommand(ADD_ROOT_FOLDER_COMMAND_ID); } } -export class GlobalRemoveRootFolderAction extends Action { +class RemoveRootFolderAction extends Action2 { - static readonly ID = 'workbench.action.removeRootFolder'; - static readonly LABEL = localize('globalRemoveFolderFromWorkspace', "Remove Folder from Workspace..."); - - constructor( - id: string, - label: string, - @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService, - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @ICommandService private readonly commandService: ICommandService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.removeRootFolder', + title: { value: localize('globalRemoveFolderFromWorkspace', "Remove Folder from Workspace..."), original: 'Remove Folder from Workspace...' }, + category: workspacesCategory, + f1: true + }); } - override async run(): Promise { - const state = this.contextService.getWorkbenchState(); + override async run(accessor: ServicesAccessor): Promise { + const contextService = accessor.get(IWorkspaceContextService); + const commandService = accessor.get(ICommandService); + const workspaceEditingService = accessor.get(IWorkspaceEditingService); + + const state = contextService.getWorkbenchState(); // Workspace / Folder if (state === WorkbenchState.WORKSPACE || state === WorkbenchState.FOLDER) { - const folder = await this.commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID); + const folder = await commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID); if (folder) { - await this.workspaceEditingService.removeFolders([folder.uri]); + await workspaceEditingService.removeFolders([folder.uri]); } } } } -export class SaveWorkspaceAsAction extends Action { +class SaveWorkspaceAsAction extends Action2 { static readonly ID = 'workbench.action.saveWorkspaceAs'; - static readonly LABEL = localize('saveWorkspaceAsAction', "Save Workspace As..."); - constructor( - id: string, - label: string, - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService - - ) { - super(id, label); + constructor() { + super({ + id: SaveWorkspaceAsAction.ID, + title: { value: localize('saveWorkspaceAsAction', "Save Workspace As..."), original: 'Save Workspace As...' }, + category: workspacesCategory, + f1: true + }); } - override async run(): Promise { - const configPathUri = await this.workspaceEditingService.pickNewWorkspacePath(); + override async run(accessor: ServicesAccessor): Promise { + const workspaceEditingService = accessor.get(IWorkspaceEditingService); + const contextService = accessor.get(IWorkspaceContextService); + + const configPathUri = await workspaceEditingService.pickNewWorkspacePath(); if (configPathUri && hasWorkspaceFileExtension(configPathUri)) { - switch (this.contextService.getWorkbenchState()) { + switch (contextService.getWorkbenchState()) { case WorkbenchState.EMPTY: case WorkbenchState.FOLDER: - const folders = this.contextService.getWorkspace().folders.map(folder => ({ uri: folder.uri })); - return this.workspaceEditingService.createAndEnterWorkspace(folders, configPathUri); + const folders = contextService.getWorkspace().folders.map(folder => ({ uri: folder.uri })); + return workspaceEditingService.createAndEnterWorkspace(folders, configPathUri); case WorkbenchState.WORKSPACE: - return this.workspaceEditingService.saveAndEnterWorkspace(configPathUri); + return workspaceEditingService.saveAndEnterWorkspace(configPathUri); } } } } -export class DuplicateWorkspaceInNewWindowAction extends Action { +class DuplicateWorkspaceInNewWindowAction extends Action2 { - static readonly ID = 'workbench.action.duplicateWorkspaceInNewWindow'; - static readonly LABEL = localize('duplicateWorkspaceInNewWindow', "Duplicate As Workspace in New Window"); - - constructor( - id: string, - label: string, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService, - @IHostService private readonly hostService: IHostService, - @IWorkspacesService private readonly workspacesService: IWorkspacesService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.duplicateWorkspaceInNewWindow', + title: { value: localize('duplicateWorkspaceInNewWindow', "Duplicate As Workspace in New Window"), original: 'Duplicate As Workspace in New Window' }, + category: workspacesCategory, + f1: true + }); } - override async run(): Promise { - const folders = this.workspaceContextService.getWorkspace().folders; - const remoteAuthority = this.environmentService.remoteAuthority; + override async run(accessor: ServicesAccessor): Promise { + const workspaceContextService = accessor.get(IWorkspaceContextService); + const workspaceEditingService = accessor.get(IWorkspaceEditingService); + const hostService = accessor.get(IHostService); + const workspacesService = accessor.get(IWorkspacesService); + const environmentService = accessor.get(IWorkbenchEnvironmentService); - const newWorkspace = await this.workspacesService.createUntitledWorkspace(folders, remoteAuthority); - await this.workspaceEditingService.copyWorkspaceSettings(newWorkspace); + const folders = workspaceContextService.getWorkspace().folders; + const remoteAuthority = environmentService.remoteAuthority; - return this.hostService.openWindow([{ workspaceUri: newWorkspace.configPath }], { forceNewWindow: true }); + const newWorkspace = await workspacesService.createUntitledWorkspace(folders, remoteAuthority); + await workspaceEditingService.copyWorkspaceSettings(newWorkspace); + + return hostService.openWindow([{ workspaceUri: newWorkspace.configPath }], { forceNewWindow: true }); } } class WorkspaceTrustManageAction extends Action2 { + constructor() { super({ id: 'workbench.action.manageTrust', title: { value: localize('manageTrustAction', "Manage Workspace Trust"), original: 'Manage Workspace Trust' }, - precondition: ContextKeyExpr.and(IsWebContext.negate(), ContextKeyExpr.equals(`config.${WORKSPACE_TRUST_ENABLED}`, true)), + precondition: ContextKeyExpr.and(WorkspaceTrustContext.IsEnabled, IsWebContext.negate(), ContextKeyExpr.equals(`config.${WORKSPACE_TRUST_ENABLED}`, true)), category: localize('workspacesCategory', "Workspaces"), f1: true }); @@ -273,14 +285,11 @@ registerAction2(WorkspaceTrustManageAction); // --- Actions Registration -const registry = Registry.as(Extensions.WorkbenchActions); -const workspacesCategory = localize('workspaces', "Workspaces"); - -registry.registerWorkbenchAction(SyncActionDescriptor.from(AddRootFolderAction), 'Workspaces: Add Folder to Workspace...', workspacesCategory); -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 As Workspace in New Window', workspacesCategory); +registerAction2(AddRootFolderAction); +registerAction2(RemoveRootFolderAction); +registerAction2(CloseWorkspaceAction); +registerAction2(SaveWorkspaceAsAction); +registerAction2(DuplicateWorkspaceInNewWindowAction); // --- Menu Registration @@ -310,7 +319,8 @@ MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: OpenWorkspaceConfigFileAction.ID, - title: { value: `${workspacesCategory}: ${OpenWorkspaceConfigFileAction.LABEL}`, original: 'Workspaces: Open Workspace Configuration File' }, + title: { value: OpenWorkspaceConfigFileAction.LABEL, original: 'Workspaces: Open Workspace Configuration File' }, + category: workspacesCategory }, when: WorkbenchStateContext.isEqualTo('workspace') }); diff --git a/src/vs/workbench/browser/actions/workspaceCommands.ts b/src/vs/workbench/browser/actions/workspaceCommands.ts index 265251bf63..449d31819c 100644 --- a/src/vs/workbench/browser/actions/workspaceCommands.ts +++ b/src/vs/workbench/browser/actions/workspaceCommands.ts @@ -20,12 +20,13 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { IFileDialogService, IPickAndOpenOptions } from 'vs/platform/dialogs/common/dialogs'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; -import { IOpenWindowOptions, IWindowOpenable } from 'vs/platform/windows/common/windows'; -import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; +import { IOpenEmptyWindowOptions, IOpenWindowOptions, IWindowOpenable } from 'vs/platform/windows/common/windows'; +import { hasWorkspaceFileExtension, IRecent, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { ILocalizedString } from 'vs/platform/actions/common/actions'; export const ADD_ROOT_FOLDER_COMMAND_ID = 'addRootFolder'; -export const ADD_ROOT_FOLDER_LABEL = localize('addFolderToWorkspace', "Add Folder to Workspace..."); +export const ADD_ROOT_FOLDER_LABEL: ILocalizedString = { value: localize('addFolderToWorkspace', "Add Folder to Workspace..."), original: 'Add Folder to Workspace...' }; export const PICK_WORKSPACE_FOLDER_COMMAND_ID = '_workbench.pickWorkspaceFolder'; @@ -184,3 +185,91 @@ CommandsRegistry.registerCommand({ ] } }); + +interface INewWindowAPICommandOptions { + reuseWindow?: boolean; + /** + * If set, defines the remoteAuthority of the new window. `null` will open a local window. + * If not set, defaults to remoteAuthority of the current window. + */ + remoteAuthority?: string | null; +} + +CommandsRegistry.registerCommand({ + id: 'vscode.newWindow', + handler: (accessor: ServicesAccessor, options?: INewWindowAPICommandOptions) => { + const commandOptions: IOpenEmptyWindowOptions = { + forceReuseWindow: options && options.reuseWindow, + remoteAuthority: options && options.remoteAuthority + }; + const commandService = accessor.get(ICommandService); + return commandService.executeCommand('_files.newWindow', commandOptions); + }, + description: { + description: 'Opens an new window depending on the newWindow argument.', + args: [ + { + name: 'options', + description: '(optional) Options. Object with the following properties: ' + + '`reuseWindow`: Whether to open a new window or the same. Defaults to opening in a new window. ', + constraint: (value: any) => value === undefined || typeof value === 'object' + } + ] + } +}); + +// recent history commands + +CommandsRegistry.registerCommand('_workbench.removeFromRecentlyOpened', function (accessor: ServicesAccessor, uri: URI) { + const workspacesService = accessor.get(IWorkspacesService); + return workspacesService.removeRecentlyOpened([uri]); +}); + + +CommandsRegistry.registerCommand({ + id: 'vscode.removeFromRecentlyOpened', + handler: (accessor: ServicesAccessor, path: string | URI): Promise => { + if (typeof path === 'string') { + path = path.match(/^[^:/?#]+:\/\//) ? URI.parse(path) : URI.file(path); + } else { + path = URI.revive(path); // called from extension host + } + const workspacesService = accessor.get(IWorkspacesService); + return workspacesService.removeRecentlyOpened([path]); + }, + description: { + description: 'Removes an entry with the given path from the recently opened list.', + args: [ + { name: 'path', description: 'URI or URI string to remove from recently opened.', constraint: (value: any) => typeof value === 'string' || value instanceof URI } + ] + } +}); + +interface RecentEntry { + uri: URI; + type: 'workspace' | 'folder' | 'file'; + label?: string; + remoteAuthority?: string; +} + +CommandsRegistry.registerCommand('_workbench.addToRecentlyOpened', async function (accessor: ServicesAccessor, recentEntry: RecentEntry) { + const workspacesService = accessor.get(IWorkspacesService); + let recent: IRecent | undefined = undefined; + const uri = recentEntry.uri; + const label = recentEntry.label; + const remoteAuthority = recentEntry.remoteAuthority; + if (recentEntry.type === 'workspace') { + const workspace = await workspacesService.getWorkspaceIdentifier(uri); + recent = { workspace, label, remoteAuthority }; + } else if (recentEntry.type === 'folder') { + recent = { folderUri: uri, label, remoteAuthority }; + } else { + recent = { fileUri: uri, label, remoteAuthority }; + } + return workspacesService.addRecentlyOpened([recent]); +}); + +CommandsRegistry.registerCommand('_workbench.getRecentlyOpened', async function (accessor: ServicesAccessor) { + const workspacesService = accessor.get(IWorkspacesService); + return workspacesService.getRecentlyOpened(); +}); diff --git a/src/vs/workbench/browser/codeeditor.ts b/src/vs/workbench/browser/codeeditor.ts index e2cb931cbf..e03af4c918 100644 --- a/src/vs/workbench/browser/codeeditor.ts +++ b/src/vs/workbench/browser/codeeditor.ts @@ -107,12 +107,14 @@ export class RangeHighlightDecorations extends Disposable { } private static readonly _WHOLE_LINE_RANGE_HIGHLIGHT = ModelDecorationOptions.register({ + description: 'codeeditor-range-highlight-whole', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'rangeHighlight', isWholeLine: true }); private static readonly _RANGE_HIGHLIGHT = ModelDecorationOptions.register({ + description: 'codeeditor-range-highlight', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'rangeHighlight' }); diff --git a/src/vs/workbench/browser/composite.ts b/src/vs/workbench/browser/composite.ts index a67f7c64fc..642cb5ea9a 100644 --- a/src/vs/workbench/browser/composite.ts +++ b/src/vs/workbench/browser/composite.ts @@ -118,10 +118,6 @@ export abstract class Composite extends Component implements IComposite { this.parent = parent; } - override updateStyles(): void { - super.updateStyles(); - } - /** * Returns the container this composite is being build in. */ @@ -158,6 +154,13 @@ export abstract class Composite extends Component implements IComposite { */ abstract layout(dimension: Dimension): void; + /** + * Update the styles of the contents of this composite. + */ + override updateStyles(): void { + super.updateStyles(); + } + /** * Returns an array of actions to show in the action bar of the composite. */ diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index fae1a01748..4b2c451829 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -7,8 +7,8 @@ import { localize } from 'vs/nls'; import { Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IContextKeyService, IContextKey, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { InputFocusedContext, IsMacContext, IsLinuxContext, IsWindowsContext, IsWebContext, IsMacNativeContext, IsDevelopmentContext } from 'vs/platform/contextkey/common/contextkeys'; -import { ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, TEXT_DIFF_EDITOR_ID, SplitEditorsVertically, InEditorZenModeContext, IsCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorReadonlyContext, EditorAreaVisibleContext, ActiveEditorAvailableEditorIdsContext } from 'vs/workbench/common/editor'; +import { InputFocusedContext, IsMacContext, IsLinuxContext, IsWindowsContext, IsWebContext, IsMacNativeContext, IsDevelopmentContext, IsIOSContext } from 'vs/platform/contextkey/common/contextkeys'; +import { ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, TEXT_DIFF_EDITOR_ID, SplitEditorsVertically, InEditorZenModeContext, IsCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorReadonlyContext, EditorAreaVisibleContext, ActiveEditorAvailableEditorIdsContext, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { trackFocus, addDisposableListener, EventType, WebFileSystemAccess } from 'vs/base/browser/dom'; import { preferredSideBySideGroupDirection, GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -22,6 +22,7 @@ import { PanelMaximizedContext, PanelPositionContext, PanelVisibleContext } from import { getRemoteName, getVirtualWorkspaceScheme } from 'vs/platform/remote/common/remoteHosts'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { isNative } from 'vs/base/common/platform'; +import { IEditorOverrideService } from 'vs/workbench/services/editor/common/editorOverrideService'; export const WorkbenchStateContext = new RawContextKey('workbenchState', undefined, { type: 'string', description: localize('workbenchState', "The kind of workspace opened in the window, either 'empty' (no workspace), 'folder' (single folder) or 'workspace' (multi-root workspace)") }); export const WorkspaceFolderCountContext = new RawContextKey('workspaceFolderCount', 0, localize('workspaceFolderCount', "The number of root folders in the workspace")); @@ -77,6 +78,7 @@ export class WorkbenchContextKeysHandler extends Disposable { @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IEditorService private readonly editorService: IEditorService, + @IEditorOverrideService private readonly editorOverrideService: IEditorOverrideService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IViewletService private readonly viewletService: IViewletService, @@ -91,6 +93,7 @@ export class WorkbenchContextKeysHandler extends Disposable { IsWebContext.bindTo(this.contextKeyService); IsMacNativeContext.bindTo(this.contextKeyService); + IsIOSContext.bindTo(this.contextKeyService); RemoteNameContext.bindTo(this.contextKeyService).set(getRemoteName(this.environmentService.remoteAuthority) || ''); @@ -239,11 +242,11 @@ export class WorkbenchContextKeysHandler extends Disposable { if (activeEditorPane) { this.activeEditorContext.set(activeEditorPane.getId()); - this.activeEditorIsReadonly.set(activeEditorPane.input.isReadonly()); + this.activeEditorIsReadonly.set(activeEditorPane.input.hasCapability(EditorInputCapabilities.Readonly)); const activeEditorResource = activeEditorPane.input.resource; - const editors = activeEditorResource ? this.editorService.getEditorOverrides(activeEditorResource, undefined, activeGroup) : []; - this.activeEditorAvailableEditorIds.set(editors.map(([_, entry]) => entry.id).join(',')); + const editors = activeEditorResource ? this.editorOverrideService.getEditorIds(activeEditorResource) : []; + this.activeEditorAvailableEditorIds.set(editors.join(',')); } else { this.activeEditorContext.reset(); this.activeEditorIsReadonly.reset(); diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 3f4d4cbf5f..bee268c4d3 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -3,73 +3,57 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Registry } from 'vs/platform/registry/common/platform'; -import { hasWorkspaceFileExtension, IWorkspaceFolderCreationData, IRecentFile, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; -import { normalize } from 'vs/base/common/path'; +import { hasWorkspaceFileExtension, IWorkspaceFolderCreationData, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { basename, isEqual } from 'vs/base/common/resources'; import { IFileService } from 'vs/platform/files/common/files'; import { IWindowOpenable } from 'vs/platform/windows/common/windows'; import { URI } from 'vs/base/common/uri'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { bufferToReadable, VSBuffer } from 'vs/base/common/buffer'; import { FileAccess, Schemas } from 'vs/base/common/network'; -import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { IBaseTextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { DataTransfers, IDragAndDropData } from 'vs/base/browser/dnd'; import { DragMouseEvent } from 'vs/base/browser/mouseEvent'; -import { normalizeDriveLetter } from 'vs/base/common/labels'; import { MIME_BINARY } from 'vs/base/common/mime'; import { isWindows } from 'vs/base/common/platform'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IEditorIdentifier, GroupIdentifier, IEditorInputFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor'; -import { IEditorService, IResourceEditorInputType } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorIdentifier, GroupIdentifier, isEditorIdentifier } from 'vs/workbench/common/editor'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Disposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { addDisposableListener, EventType } from 'vs/base/browser/dom'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; import { withNullAsUndefined } from 'vs/base/common/types'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { Emitter } from 'vs/base/common/event'; -import { NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { coalesce } from 'vs/base/common/arrays'; +import { parse, stringify } from 'vs/base/common/marshalling'; +import { ILabelService } from 'vs/platform/label/common/label'; -export interface IDraggedResource { - resource: URI; - isExternal: boolean; -} - -interface ISerializedDraggedResource { - resource: string; -} +//#region Editor / Resources DND export class DraggedEditorIdentifier { - constructor(public readonly identifier: IEditorIdentifier) { } + constructor(readonly identifier: IEditorIdentifier) { } } export class DraggedEditorGroupIdentifier { - constructor(public readonly identifier: GroupIdentifier) { } + constructor(readonly identifier: GroupIdentifier) { } } -interface IDraggedEditorProps { - dirtyContent?: string; - encoding?: string; - mode?: string; - options?: ITextEditorOptions; -} - -export interface IDraggedEditor extends IDraggedResource, IDraggedEditorProps { } - -export interface ISerializedDraggedEditor extends ISerializedDraggedResource, IDraggedEditorProps { } - export const CodeDataTransfers = { EDITORS: 'CodeEditors', FILES: 'CodeFiles' }; -export function extractResources(e: DragEvent, externalOnly?: boolean): Array { - const resources: Array = []; +export interface IDraggedResourceEditorInput extends IBaseTextResourceEditorInput { + resource?: URI; + isExternal?: boolean; +} + +export function extractEditorsDropData(e: DragEvent, externalOnly?: boolean): Array { + const editors: IDraggedResourceEditorInput[] = []; if (e.dataTransfer && e.dataTransfer.types.length > 0) { // Check for window-to-window DND @@ -79,17 +63,7 @@ export function extractResources(e: DragEvent, externalOnly?: boolean): Array { - resources.push({ - resource: URI.parse(draggedEditor.resource), - dirtyContent: draggedEditor.dirtyContent, - options: draggedEditor.options, - encoding: draggedEditor.encoding, - mode: draggedEditor.mode, - isExternal: false - }); - }); + editors.push(...parse(rawEditorsData)); } catch (error) { // Invalid transfer } @@ -100,8 +74,12 @@ export function extractResources(e: DragEvent, externalOnly?: boolean): Array ({ resource: URI.parse(uriStr), isExternal: false }))); + const resourcesRaw: string[] = JSON.parse(rawResourcesData); + for (const resourceRaw of resourcesRaw) { + if (resourceRaw.indexOf(':') > 0) { // mitigate https://github.com/microsoft/vscode/issues/124946 + editors.push({ resource: URI.parse(resourceRaw) }); + } + } } } catch (error) { // Invalid transfer @@ -110,12 +88,12 @@ export function extractResources(e: DragEvent, externalOnly?: boolean): Array resource.resource.fsPath === file.path) /* prevent duplicates */) { + if (file?.path /* Electron only */) { try { - resources.push({ resource: URI.file(file.path), isExternal: true }); + editors.push({ resource: URI.file(file.path), isExternal: true }); } catch (error) { // Invalid URI } @@ -128,18 +106,16 @@ export function extractResources(e: DragEvent, externalOnly?: boolean): Array { - if (!resources.some(resource => resource.resource.fsPath === codeFile) /* prevent duplicates */) { - resources.push({ resource: URI.file(codeFile), isExternal: true }); - } - }); + for (const codeFile of codeFiles) { + editors.push({ resource: URI.file(codeFile), isExternal: true }); + } } catch (error) { // Invalid transfer } } } - return resources; + return editors; } export interface IResourcesDropHandlerOptions { @@ -148,21 +124,20 @@ export interface IResourcesDropHandlerOptions { * Whether to open the actual workspace when a workspace configuration file is dropped * or whether to open the configuration file within the editor as normal file. */ - allowWorkspaceOpen: boolean; + readonly allowWorkspaceOpen: boolean; } /** - * Shared function across some components to handle drag & drop of resources. E.g. of folders and workspace files - * to open them in the window instead of the editor or to handle dirty editors being dropped between instances of Code. + * Shared function across some components to handle drag & drop of resources. + * E.g. of folders and workspace files to open them in the window instead of + * the editor or to handle dirty editors being dropped between instances of Code. */ export class ResourcesDropHandler { constructor( - private options: IResourcesDropHandlerOptions, + private readonly options: IResourcesDropHandlerOptions, @IFileService private readonly fileService: IFileService, @IWorkspacesService private readonly workspacesService: IWorkspacesService, - @ITextFileService private readonly textFileService: ITextFileService, - @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, @IEditorService private readonly editorService: IEditorService, @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService, @IHostService private readonly hostService: IHostService @@ -170,110 +145,61 @@ export class ResourcesDropHandler { } async handleDrop(event: DragEvent, resolveTargetGroup: () => IEditorGroup | undefined, afterDrop: (targetGroup: IEditorGroup | undefined) => void, targetIndex?: number): Promise { - const untitledOrFileResources = extractResources(event).filter(resource => this.fileService.canHandleResource(resource.resource) || resource.resource.scheme === Schemas.untitled); - if (!untitledOrFileResources.length) { + const editors = extractEditorsDropData(event); + if (!editors.length) { return; } // Make the window active to handle the drop properly within await this.hostService.focus(); - // Check for special things being dropped - const isWorkspaceOpening = await this.doHandleDrop(untitledOrFileResources); - if (isWorkspaceOpening) { - return; // return early if the drop operation resulted in this window changing to a workspace + // Check for workspace file being dropped if we are allowed to do so + const externalLocalFiles = coalesce(editors.filter(editor => editor.isExternal && editor.resource?.scheme === Schemas.file).map(editor => editor.resource)); + if (this.options.allowWorkspaceOpen) { + if (externalLocalFiles.length > 0) { + const isWorkspaceOpening = await this.handleWorkspaceFileDrop(externalLocalFiles); + if (isWorkspaceOpening) { + return; // return early if the drop operation resulted in this window changing to a workspace + } + } } // Add external ones to recently open list unless dropped resource is a workspace - const recentFiles: IRecentFile[] = untitledOrFileResources.filter(untitledOrFileResource => untitledOrFileResource.isExternal && untitledOrFileResource.resource.scheme === Schemas.file).map(d => ({ fileUri: d.resource })); - if (recentFiles.length) { - this.workspacesService.addRecentlyOpened(recentFiles); + if (externalLocalFiles.length) { + this.workspacesService.addRecentlyOpened(externalLocalFiles.map(resource => ({ fileUri: resource }))); } - const editors: IResourceEditorInputType[] = untitledOrFileResources.map(untitledOrFileResource => ({ - resource: untitledOrFileResource.resource, - encoding: (untitledOrFileResource as IDraggedEditor).encoding, - mode: (untitledOrFileResource as IDraggedEditor).mode, - options: { - ...(untitledOrFileResource as IDraggedEditor).options, - pinned: true, - index: targetIndex - } - })); - // Open in Editor const targetGroup = resolveTargetGroup(); - await this.editorService.openEditors(editors, targetGroup); + await this.editorService.openEditors(editors.map(editor => ({ + ...editor, + options: { + ...editor.options, + pinned: true, + index: targetIndex + } + })), targetGroup, { validateTrust: true }); // Finish with provided function afterDrop(targetGroup); } - private async doHandleDrop(untitledOrFileResources: Array): Promise { - - // Check for dirty editors being dropped - const dirtyEditors: IDraggedEditor[] = untitledOrFileResources.filter(untitledOrFileResource => !untitledOrFileResource.isExternal && typeof (untitledOrFileResource as IDraggedEditor).dirtyContent === 'string'); - if (dirtyEditors.length > 0) { - await Promise.all(dirtyEditors.map(dirtyEditor => this.handleDirtyEditorDrop(dirtyEditor))); - return false; - } - - // Check for workspace file being dropped if we are allowed to do so - if (this.options.allowWorkspaceOpen) { - const externalFileOnDiskResources = untitledOrFileResources.filter(untitledOrFileResource => untitledOrFileResource.isExternal && untitledOrFileResource.resource.scheme === Schemas.file).map(d => d.resource); - if (externalFileOnDiskResources.length > 0) { - return this.handleWorkspaceFileDrop(externalFileOnDiskResources); - } - } - - return false; - } - - private async handleDirtyEditorDrop(droppedDirtyEditor: IDraggedEditor): Promise { - const fileEditorFactory = Registry.as(EditorExtensions.EditorInputFactories).getFileEditorInputFactory(); - - // Untitled: always ensure that we open a new untitled editor for each file we drop - if (droppedDirtyEditor.resource.scheme === Schemas.untitled) { - const untitledEditorResource = this.editorService.createEditorInput({ mode: droppedDirtyEditor.mode, encoding: droppedDirtyEditor.encoding, forceUntitled: true }).resource; - if (untitledEditorResource) { - droppedDirtyEditor.resource = untitledEditorResource; - } - } - - // File: ensure the file is not dirty or opened already - else if (this.textFileService.isDirty(droppedDirtyEditor.resource) || this.editorService.isOpened({ resource: droppedDirtyEditor.resource, typeId: fileEditorFactory.typeId })) { - return false; - } - - // If the dropped editor is dirty with content we simply take that - // content and turn it into a backup so that it loads the contents - if (typeof droppedDirtyEditor.dirtyContent === 'string') { - try { - await this.workingCopyBackupService.backup({ resource: droppedDirtyEditor.resource, typeId: NO_TYPE_ID }, bufferToReadable(VSBuffer.fromString(droppedDirtyEditor.dirtyContent))); - } catch (e) { - // Ignore error - } - } - - return false; - } - - private async handleWorkspaceFileDrop(fileOnDiskResources: URI[]): Promise { + private async handleWorkspaceFileDrop(resources: URI[]): Promise { const toOpen: IWindowOpenable[] = []; const folderURIs: IWorkspaceFolderCreationData[] = []; - await Promise.all(fileOnDiskResources.map(async fileOnDiskResource => { + await Promise.all(resources.map(async resource => { // Check for Workspace - if (hasWorkspaceFileExtension(fileOnDiskResource)) { - toOpen.push({ workspaceUri: fileOnDiskResource }); + if (hasWorkspaceFileExtension(resource)) { + toOpen.push({ workspaceUri: resource }); return; } // Check for Folder try { - const stat = await this.fileService.resolve(fileOnDiskResource); + const stat = await this.fileService.resolve(resource); if (stat.isDirectory) { toOpen.push({ folderUri: stat.resource }); folderURIs.push({ uri: stat.resource }); @@ -305,92 +231,141 @@ export class ResourcesDropHandler { } } -export function fillResourceDataTransfers(accessor: ServicesAccessor, resources: (URI | { resource: URI, isDirectory: boolean })[], optionsCallback: ((resource: URI) => ITextEditorOptions) | undefined, event: DragMouseEvent | DragEvent): void { - if (resources.length === 0 || !event.dataTransfer) { +interface IResourceStat { + resource: URI; + isDirectory?: boolean; +} + +export function fillEditorsDragData(accessor: ServicesAccessor, resources: URI[], event: DragMouseEvent | DragEvent): void; +export function fillEditorsDragData(accessor: ServicesAccessor, resources: IResourceStat[], event: DragMouseEvent | DragEvent): void; +export function fillEditorsDragData(accessor: ServicesAccessor, editors: IEditorIdentifier[], event: DragMouseEvent | DragEvent): void; +export function fillEditorsDragData(accessor: ServicesAccessor, resourcesOrEditors: Array, event: DragMouseEvent | DragEvent): void { + if (resourcesOrEditors.length === 0 || !event.dataTransfer) { return; } - const sources = resources.map(obj => { - if (URI.isUri(obj)) { - return { resource: obj, isDirectory: false /* assume resource is not a directory */ }; + const textFileService = accessor.get(ITextFileService); + const editorService = accessor.get(IEditorService); + const fileService = accessor.get(IFileService); + const labelService = accessor.get(ILabelService); + + // Extract resources from URIs or Editors that + // can be handled by the file service + const fileSystemResources = coalesce(resourcesOrEditors.map(resourceOrEditor => { + if (URI.isUri(resourceOrEditor)) { + return { resource: resourceOrEditor }; } - return obj; - }); + if (isEditorIdentifier(resourceOrEditor)) { + if (URI.isUri(resourceOrEditor.editor.resource)) { + return { resource: resourceOrEditor.editor.resource }; + } + + return undefined; // editor without resource + } + + return resourceOrEditor; + })).filter(({ resource }) => fileService.canHandleResource(resource)); // Text: allows to paste into text-capable areas const lineDelimiter = isWindows ? '\r\n' : '\n'; - event.dataTransfer.setData(DataTransfers.TEXT, sources.map(source => source.resource.scheme === Schemas.file ? normalize(normalizeDriveLetter(source.resource.fsPath)) : source.resource.toString()).join(lineDelimiter)); + event.dataTransfer.setData(DataTransfers.TEXT, fileSystemResources.map(({ resource }) => labelService.getUriLabel(resource, { noPrefix: true })).join(lineDelimiter)); // Download URL: enables support to drag a tab as file to desktop (only single file supported) - if (!sources[0].isDirectory) { - event.dataTransfer.setData(DataTransfers.DOWNLOAD_URL, [MIME_BINARY, basename(sources[0].resource), FileAccess.asBrowserUri(sources[0].resource).toString()].join(':')); + const firstFile = fileSystemResources.find(({ isDirectory }) => !isDirectory); + if (firstFile) { + // TODO@sandbox this will no longer work when `vscode-file` + // is enabled because we block loading resources that are not + // inside installation dir + event.dataTransfer.setData(DataTransfers.DOWNLOAD_URL, [MIME_BINARY, basename(firstFile.resource), FileAccess.asBrowserUri(firstFile.resource).toString()].join(':')); } - // Resource URLs: allows to drop multiple resources to a target in VS Code (not directories) - const files = sources.filter(source => !source.isDirectory); + // Resource URLs: allows to drop multiple file resources to a target in VS Code + const files = fileSystemResources.filter(({ isDirectory }) => !isDirectory); if (files.length) { - event.dataTransfer.setData(DataTransfers.RESOURCES, JSON.stringify(files.map(file => file.resource.toString()))); + event.dataTransfer.setData(DataTransfers.RESOURCES, JSON.stringify(files.map(({ resource }) => resource.toString()))); } - // Editors: enables cross window DND of tabs into the editor area - const textFileService = accessor.get(ITextFileService); - const editorService = accessor.get(IEditorService); + // Editors: enables cross window DND of editors + // into the editor area while presering UI state + const draggedEditors: IDraggedResourceEditorInput[] = []; - const draggedEditors: ISerializedDraggedEditor[] = []; - files.forEach(file => { - let options: ITextEditorOptions | undefined = undefined; + for (const resourceOrEditor of resourcesOrEditors) { - // Use provided callback for editor options - if (typeof optionsCallback === 'function') { - options = optionsCallback(file.resource); + // Extract resource editor from provided object or URI + let editor: IDraggedResourceEditorInput | undefined = undefined; + if (isEditorIdentifier(resourceOrEditor)) { + editor = resourceOrEditor.editor.asResourceEditorInput(resourceOrEditor.groupId); + } else if (URI.isUri(resourceOrEditor)) { + editor = { resource: resourceOrEditor }; + } else if (!resourceOrEditor.isDirectory) { + editor = { resource: resourceOrEditor.resource }; } - // Otherwise try to figure out the view state from opened editors that match - else { - options = { - viewState: (() => { - const textEditorControls = editorService.visibleTextEditorControls; - for (const textEditorControl of textEditorControls) { - if (isCodeEditor(textEditorControl)) { - const model = textEditorControl.getModel(); - if (isEqual(model?.uri, file.resource)) { - return withNullAsUndefined(textEditorControl.saveViewState()); - } - } + if (!editor) { + continue; // skip over editors that cannot be transferred via dnd + } + + // Fill in some properties if they are not there already by accessing + // some well known things from the text file universe. + // This is not ideal for custom editors, but those have a chance to + // provide everything from the `asResourceEditorInput` method. + { + const resource = editor.resource; + if (resource) { + const textFileModel = textFileService.files.get(resource); + if (textFileModel) { + + // mode + if (typeof editor.mode !== 'string') { + editor.mode = textFileModel.getMode(); } - return undefined; - })() - }; - } + // encoding + if (typeof editor.encoding !== 'string') { + editor.encoding = textFileModel.getEncoding(); + } - // Try to find encoding and mode from text model - let encoding: string | undefined = undefined; - let mode: string | undefined = undefined; + // contents (only if dirty) + if (typeof editor.contents !== 'string' && textFileModel.isDirty()) { + editor.contents = textFileModel.textEditorModel.getValue(); + } + } - const model = file.resource.scheme === Schemas.untitled ? textFileService.untitled.get(file.resource) : textFileService.files.get(file.resource); - if (model) { - encoding = model.getEncoding(); - mode = model.getMode(); - } + // viewState + if (!editor.options?.viewState) { + editor.options = { + ...editor.options, + viewState: (() => { + for (const textEditorControl of editorService.visibleTextEditorControls) { + if (isCodeEditor(textEditorControl)) { + const model = textEditorControl.getModel(); + if (isEqual(model?.uri, resource)) { + return withNullAsUndefined(textEditorControl.saveViewState()); + } + } + } - // If the resource is dirty or untitled, send over its content - // to restore dirty state. Get that from the text model directly - let dirtyContent: string | undefined = undefined; - if (model?.isDirty()) { - dirtyContent = model.textEditorModel!.getValue(); // {{SQL CARBON EDIT}} strict-null-checks + return undefined; + })() + }; + } + } } // Add as dragged editor - draggedEditors.push({ resource: file.resource.toString(), dirtyContent, options, encoding, mode }); - }); + draggedEditors.push(editor); + } if (draggedEditors.length) { - event.dataTransfer.setData(CodeDataTransfers.EDITORS, JSON.stringify(draggedEditors)); + event.dataTransfer.setData(CodeDataTransfers.EDITORS, stringify(draggedEditors)); } } +//#endregion + +//#region DND Utilities + /** * A singleton to store transfer data during drag & drop operations that are only valid within the application. */ @@ -437,12 +412,12 @@ export class LocalSelectionTransfer { } export interface IDragAndDropObserverCallbacks { - onDragEnter: (e: DragEvent) => void; - onDragLeave: (e: DragEvent) => void; - onDrop: (e: DragEvent) => void; - onDragEnd: (e: DragEvent) => void; + readonly onDragEnter: (e: DragEvent) => void; + readonly onDragLeave: (e: DragEvent) => void; + readonly onDrop: (e: DragEvent) => void; + readonly onDragEnd: (e: DragEvent) => void; - onDragOver?: (e: DragEvent) => void; + readonly onDragOver?: (e: DragEvent) => void; } export class DragAndDropObserver extends Disposable { @@ -453,7 +428,7 @@ export class DragAndDropObserver extends Disposable { // repeadedly. private counter: number = 0; - constructor(private element: HTMLElement, private callbacks: IDragAndDropObserverCallbacks) { + constructor(private readonly element: HTMLElement, private readonly callbacks: IDragAndDropObserverCallbacks) { super(); this.registerListeners(); @@ -514,7 +489,14 @@ export function containsDragType(event: DragEvent, ...dragTypesToFind: string[]) return false; } -export type Before2D = { verticallyBefore: boolean; horizontallyBefore: boolean; }; +//#endregion + +//#region Composites DND + +export type Before2D = { + readonly verticallyBefore: boolean; + readonly horizontallyBefore: boolean; +}; export interface ICompositeDragAndDrop { drop(data: IDragAndDropData, target: string | undefined, originalEvent: DragEvent, before?: Before2D): void; @@ -532,10 +514,13 @@ export interface ICompositeDragAndDropObserverCallbacks { } export class CompositeDragAndDropData implements IDragAndDropData { + constructor(private type: 'view' | 'composite', private id: string) { } + update(dataTransfer: DataTransfer): void { // no-op } + getData(): { type: 'view' | 'composite'; id: string; @@ -545,44 +530,51 @@ export class CompositeDragAndDropData implements IDragAndDropData { } export interface IDraggedCompositeData { - eventData: DragEvent; - dragAndDropData: CompositeDragAndDropData; + readonly eventData: DragEvent; + readonly dragAndDropData: CompositeDragAndDropData; } export class DraggedCompositeIdentifier { - constructor(private _compositeId: string) { } + + constructor(private compositeId: string) { } get id(): string { - return this._compositeId; + return this.compositeId; } } export class DraggedViewIdentifier { - constructor(private _viewId: string) { } + + constructor(private viewId: string) { } get id(): string { - return this._viewId; + return this.viewId; } } export type ViewType = 'composite' | 'view'; export class CompositeDragAndDropObserver extends Disposable { - private transferData: LocalSelectionTransfer; - private _onDragStart = this._register(new Emitter()); - private _onDragEnd = this._register(new Emitter()); - private static _instance: CompositeDragAndDropObserver | undefined; + + private static instance: CompositeDragAndDropObserver | undefined; + static get INSTANCE(): CompositeDragAndDropObserver { - if (!CompositeDragAndDropObserver._instance) { - CompositeDragAndDropObserver._instance = new CompositeDragAndDropObserver(); + if (!CompositeDragAndDropObserver.instance) { + CompositeDragAndDropObserver.instance = new CompositeDragAndDropObserver(); } - return CompositeDragAndDropObserver._instance; + + return CompositeDragAndDropObserver.instance; } + + private readonly transferData = LocalSelectionTransfer.getInstance(); + + private readonly onDragStart = this._register(new Emitter()); + private readonly onDragEnd = this._register(new Emitter()); + private constructor() { super(); - this.transferData = LocalSelectionTransfer.getInstance(); - this._register(this._onDragEnd.event(e => { + this._register(this.onDragEnd.event(e => { const id = e.dragAndDropData.getData().id; const type = e.dragAndDropData.getData().type; const data = this.readDragData(type); @@ -591,6 +583,7 @@ export class CompositeDragAndDropObserver extends Disposable { } })); } + private readDragData(type: ViewType): CompositeDragAndDropData | undefined { if (this.transferData.hasData(type === 'view' ? DraggedViewIdentifier.prototype : DraggedCompositeIdentifier.prototype)) { const data = this.transferData.getData(type === 'view' ? DraggedViewIdentifier.prototype : DraggedCompositeIdentifier.prototype); @@ -598,11 +591,14 @@ export class CompositeDragAndDropObserver extends Disposable { return new CompositeDragAndDropData(type, data[0].id); } } + return undefined; } + private writeDragData(id: string, type: ViewType): void { this.transferData.setData([type === 'view' ? new DraggedViewIdentifier(id) : new DraggedCompositeIdentifier(id)], type === 'view' ? DraggedViewIdentifier.prototype : DraggedCompositeIdentifier.prototype); } + registerTarget(element: HTMLElement, callbacks: ICompositeDragAndDropObserverCallbacks): IDisposable { const disposableStore = new DisposableStore(); disposableStore.add(new DragAndDropObserver(element, { @@ -611,6 +607,7 @@ export class CompositeDragAndDropObserver extends Disposable { }, onDragEnter: e => { e.preventDefault(); + if (callbacks.onDragEnter) { const data = this.readDragData('composite') || this.readDragData('view'); if (data) { @@ -634,11 +631,12 @@ export class CompositeDragAndDropObserver extends Disposable { callbacks.onDrop({ eventData: e, dragAndDropData: data! }); // Fire drag event in case drop handler destroys the dragged element - this._onDragEnd.fire({ eventData: e, dragAndDropData: data! }); + this.onDragEnd.fire({ eventData: e, dragAndDropData: data! }); } }, onDragOver: e => { e.preventDefault(); + if (callbacks.onDragOver) { const data = this.readDragData('composite') || this.readDragData('view'); if (!data) { @@ -649,45 +647,47 @@ export class CompositeDragAndDropObserver extends Disposable { } } })); + if (callbacks.onDragStart) { - this._onDragStart.event(e => { + this.onDragStart.event(e => { callbacks.onDragStart!(e); }, this, disposableStore); } + if (callbacks.onDragEnd) { - this._onDragEnd.event(e => { + this.onDragEnd.event(e => { callbacks.onDragEnd!(e); }); } + return this._register(disposableStore); } registerDraggable(element: HTMLElement, draggedItemProvider: () => { type: ViewType, id: string }, callbacks: ICompositeDragAndDropObserverCallbacks): IDisposable { element.draggable = true; + const disposableStore = new DisposableStore(); + disposableStore.add(addDisposableListener(element, EventType.DRAG_START, e => { const { id, type } = draggedItemProvider(); this.writeDragData(id, type); - if (e.dataTransfer) { - e.dataTransfer.setDragImage(element, 0, 0); - } + e.dataTransfer?.setDragImage(element, 0, 0); - this._onDragStart.fire({ eventData: e, dragAndDropData: this.readDragData(type)! }); + this.onDragStart.fire({ eventData: e, dragAndDropData: this.readDragData(type)! }); })); + disposableStore.add(new DragAndDropObserver(element, { onDragEnd: e => { const { type } = draggedItemProvider(); const data = this.readDragData(type); - if (!data) { return; } - this._onDragEnd.fire({ eventData: e, dragAndDropData: data! }); + this.onDragEnd.fire({ eventData: e, dragAndDropData: data! }); }, onDragEnter: e => { - if (callbacks.onDragEnter) { const data = this.readDragData('composite') || this.readDragData('view'); if (!data) { @@ -712,14 +712,14 @@ export class CompositeDragAndDropObserver extends Disposable { onDrop: e => { if (callbacks.onDrop) { const data = this.readDragData('composite') || this.readDragData('view'); - if (!data) { return; } + callbacks.onDrop({ eventData: e, dragAndDropData: data! }); // Fire drag event in case drop handler destroys the dragged element - this._onDragEnd.fire({ eventData: e, dragAndDropData: data! }); + this.onDragEnd.fire({ eventData: e, dragAndDropData: data! }); } }, onDragOver: e => { @@ -733,16 +733,19 @@ export class CompositeDragAndDropObserver extends Disposable { } } })); + if (callbacks.onDragStart) { - this._onDragStart.event(e => { + this.onDragStart.event(e => { callbacks.onDragStart!(e); }, this, disposableStore); } + if (callbacks.onDragEnd) { - this._onDragEnd.event(e => { + this.onDragEnd.event(e => { callbacks.onDragEnd!(e); }, this, disposableStore); } + return this._register(disposableStore); } } @@ -754,3 +757,5 @@ export function toggleDropEffect(dataTransfer: DataTransfer | null, dropEffect: dataTransfer.dropEffect = shouldHaveIt ? dropEffect : 'none'; } + +//#endregion diff --git a/src/vs/workbench/browser/editor.ts b/src/vs/workbench/browser/editor.ts index 2dfbbeec38..22e41c9eb0 100644 --- a/src/vs/workbench/browser/editor.ts +++ b/src/vs/workbench/browser/editor.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { EditorInput, EditorResourceAccessor, IEditorInput, IEditorInputFactoryRegistry, SideBySideEditorInput, EditorExtensions } from 'vs/workbench/common/editor'; +import { EditorResourceAccessor, IEditorInput, EditorExtensions, SideBySideEditor, IEditorDescriptor as ICommonEditorDescriptor } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; @@ -15,28 +16,12 @@ import { Promises } from 'vs/base/common/async'; import { IEditorService } 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 { NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { URI } from 'vs/workbench/workbench.web.api'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; //#region Editors Registry -export interface IEditorDescriptor { - - /** - * The unique identifier of the editor - */ - getId(): string; - - /** - * The display name of the editor - */ - getName(): string; - - instantiate(instantiationService: IInstantiationService): EditorPane; - - describes(obj: unknown): boolean; -} +export interface IEditorDescriptor extends ICommonEditorDescriptor { } export interface IEditorRegistry { @@ -49,22 +34,12 @@ export interface IEditorRegistry { * @param inputDescriptors A set of constructor functions that return an instance of EditorInput for which the * registered editor should be used for. */ - registerEditor(descriptor: IEditorDescriptor, inputDescriptors: readonly SyncDescriptor[]): IDisposable; + registerEditor(editorDescriptor: IEditorDescriptor, inputDescriptors: readonly SyncDescriptor[]): IDisposable; /** * Returns the editor descriptor for the given input or `undefined` if none. */ getEditor(input: EditorInput): IEditorDescriptor | undefined; - - /** - * Returns the editor descriptor for the given identifier or `undefined` if none. - */ - getEditorById(editorId: string): IEditorDescriptor | undefined; - - /** - * Returns an array of registered editors known to the platform. - */ - getEditors(): readonly IEditorDescriptor[]; } /** @@ -75,36 +50,28 @@ export class EditorDescriptor implements IEditorDescriptor { static create( ctor: { new(...services: Services): EditorPane }, - id: string, + typeId: string, name: string ): EditorDescriptor { - return new EditorDescriptor(ctor as IConstructorSignature0, id, name); + return new EditorDescriptor(ctor as IConstructorSignature0, typeId, name); } - constructor( + private constructor( private readonly ctor: IConstructorSignature0, - private readonly id: string, - private readonly name: string + readonly typeId: string, + readonly name: string ) { } instantiate(instantiationService: IInstantiationService): EditorPane { return instantiationService.createInstance(this.ctor); } - getId(): string { - return this.id; - } - - getName(): string { - return this.name; - } - - describes(obj: unknown): boolean { - return obj instanceof EditorPane && obj.getId() === this.id; + describes(editorPane: EditorPane): boolean { + return editorPane.getId() === this.typeId; } } -class EditorRegistry implements IEditorRegistry { +export class EditorRegistry implements IEditorRegistry { private readonly editors: EditorDescriptor[] = []; private readonly mapEditorToInputs = new Map[]>(); @@ -121,58 +88,53 @@ class EditorRegistry implements IEditorRegistry { } getEditor(input: EditorInput): EditorDescriptor | undefined { - const findEditorDescriptors = (input: EditorInput, byInstanceOf?: boolean): EditorDescriptor[] => { - const matchingDescriptors: EditorDescriptor[] = []; + const descriptors = this.findEditorDescriptors(input); - for (const editor of this.editors) { - const inputDescriptors = this.mapEditorToInputs.get(editor) || []; - for (const inputDescriptor of inputDescriptors) { - const inputClass = inputDescriptor.ctor; + if (descriptors.length === 0) { + return undefined; + } - // Direct check on constructor type (ignores prototype chain) - if (!byInstanceOf && input.constructor === inputClass) { - matchingDescriptors.push(editor); - break; - } - - // Normal instanceof check - else if (byInstanceOf && input instanceof inputClass) { - matchingDescriptors.push(editor); - break; - } - } - } - - // If no descriptors found, continue search using instanceof and prototype chain - if (!byInstanceOf && matchingDescriptors.length === 0) { - return findEditorDescriptors(input, true); - } - - if (byInstanceOf) { - return matchingDescriptors; - } - - return matchingDescriptors; - }; - - const descriptors = findEditorDescriptors(input); - if (descriptors.length > 0) { - - // Ask the input for its preferred Editor - const preferredEditorId = input.getPreferredEditorId(descriptors.map(descriptor => descriptor.getId())); - if (preferredEditorId) { - return this.getEditorById(preferredEditorId); - } - - // Otherwise, first come first serve + if (descriptors.length === 1) { return descriptors[0]; } - return undefined; + return input.prefersEditor(descriptors); } - getEditorById(editorId: string): EditorDescriptor | undefined { - return this.editors.find(editor => editor.getId() === editorId); + private findEditorDescriptors(input: EditorInput, byInstanceOf?: boolean): EditorDescriptor[] { + const matchingDescriptors: EditorDescriptor[] = []; + + for (const editor of this.editors) { + const inputDescriptors = this.mapEditorToInputs.get(editor) || []; + for (const inputDescriptor of inputDescriptors) { + const inputClass = inputDescriptor.ctor; + + // Direct check on constructor type (ignores prototype chain) + if (!byInstanceOf && input.constructor === inputClass) { + matchingDescriptors.push(editor); + break; + } + + // Normal instanceof check + else if (byInstanceOf && input instanceof inputClass) { + matchingDescriptors.push(editor); + break; + } + } + } + + // If no descriptors found, continue search using instanceof and prototype chain + if (!byInstanceOf && matchingDescriptors.length === 0) { + return this.findEditorDescriptors(input, true); + } + + return matchingDescriptors; + } + + //#region Used for tests only + + getEditorByType(typeId: string): EditorDescriptor | undefined { + return this.editors.find(editor => editor.typeId === typeId); } getEditors(): readonly EditorDescriptor[] { @@ -190,45 +152,28 @@ class EditorRegistry implements IEditorRegistry { return inputClasses; } + + //#endregion } Registry.add(EditorExtensions.Editors, new EditorRegistry()); //#endregion -//#region Text Editor Close Tracker +//#region Editor Close Tracker -export function whenTextEditorClosed(accessor: ServicesAccessor, resources: URI[]): Promise { +export function whenEditorClosed(accessor: ServicesAccessor, resources: URI[]): Promise { const editorService = accessor.get(IEditorService); const uriIdentityService = accessor.get(IUriIdentityService); const workingCopyService = accessor.get(IWorkingCopyService); - const fileEditorInputFactory = Registry.as(EditorExtensions.EditorInputFactories).getFileEditorInputFactory(); - return new Promise(resolve => { let remainingResources = [...resources]; // Observe any editor closing from this moment on const listener = editorService.onDidCloseEditor(async event => { - let primaryResource: URI | undefined = undefined; - let secondaryResource: URI | undefined = undefined; - - // Resolve the resources from the editor that closed - // but only consider file editor inputs, given we - // are only tracking text editors. - if (event.editor instanceof SideBySideEditorInput) { - if (fileEditorInputFactory.isFileEditorInput(event.editor.primary)) { - primaryResource = EditorResourceAccessor.getOriginalUri(event.editor.primary); - } - - if (fileEditorInputFactory.isFileEditorInput(event.editor.secondary)) { - secondaryResource = EditorResourceAccessor.getOriginalUri(event.editor.secondary); - } - } else { - if (fileEditorInputFactory.isFileEditorInput(event.editor)) { - primaryResource = EditorResourceAccessor.getOriginalUri(event.editor); - } - } + const primaryResource = EditorResourceAccessor.getOriginalUri(event.editor, { supportSideBySide: SideBySideEditor.PRIMARY }); + const secondaryResource = EditorResourceAccessor.getOriginalUri(event.editor, { supportSideBySide: SideBySideEditor.SECONDARY }); // Remove from resources to wait for being closed based on the // resources from editors that got closed @@ -247,10 +192,10 @@ export function whenTextEditorClosed(accessor: ServicesAccessor, resources: URI[ // to close the editor while the save still continues in the background. As such // we have to also check if the editors to track for are dirty and if so wait // for them to get saved. - const dirtyResources = resources.filter(resource => workingCopyService.isDirty(resource, NO_TYPE_ID /* only check on text file working copies */)); + const dirtyResources = resources.filter(resource => workingCopyService.isDirty(resource)); if (dirtyResources.length > 0) { await Promises.settled(dirtyResources.map(async resource => await new Promise(resolve => { - if (!workingCopyService.isDirty(resource, NO_TYPE_ID /* only check on text file working copies */)) { + if (!workingCopyService.isDirty(resource)) { return resolve(); // return early if resource is not dirty } @@ -275,7 +220,6 @@ export function whenTextEditorClosed(accessor: ServicesAccessor, resources: URI[ //#endregion - //#region ARIA export function computeEditorAriaLabel(input: IEditorInput, index: number | undefined, group: IEditorGroup | undefined, groupCount: number): string { diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 887eb51867..d97d436677 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -289,7 +289,7 @@ class ResourceLabelWidget extends IconLabel { @IModelService private readonly modelService: IModelService, @IDecorationsService private readonly decorationsService: IDecorationsService, @ILabelService private readonly labelService: ILabelService, - // @ITextFileService private readonly textFileService: ITextFileService, + // @ITextFileService private readonly textFileService: ITextFileService, {{SQL CARBON EDIT}} @IWorkspaceContextService private readonly contextService: IWorkspaceContextService ) { super(container, options); diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index fcfe1d8772..ee999cdd8f 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -9,8 +9,9 @@ import { EventType, addDisposableListener, getClientArea, Dimension, position, s import { onDidChangeFullscreen, isFullscreen } from 'vs/base/browser/browser'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { Registry } from 'vs/platform/registry/common/platform'; -import { isWindows, isLinux, isMacintosh, isWeb, isNative } from 'vs/base/common/platform'; -import { pathsToEditors, SideBySideEditorInput } from 'vs/workbench/common/editor'; +import { isWindows, isLinux, isMacintosh, isWeb, isNative, isIOS } from 'vs/base/common/platform'; +import { IResourceDiffEditorInput, pathsToEditors } from 'vs/workbench/common/editor'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { SidebarPart } from 'vs/workbench/browser/parts/sidebar/sidebarPart'; import { PanelPart } from 'vs/workbench/browser/parts/panel/panelPart'; import { PanelRegistry, Extensions as PanelExtensions } from 'vs/workbench/browser/panel'; @@ -48,6 +49,7 @@ 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'; +import { IBannerService } from 'vs/workbench/services/banner/browser/bannerService'; export enum Settings { ACTIVITYBAR_VISIBLE = 'workbench.activityBar.visible', @@ -150,6 +152,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private disposed: boolean | undefined; private titleBarPartView!: ISerializableView; + private bannerPartView!: ISerializableView; private activityBarPartView!: ISerializableView; private sideBarPartView!: ISerializableView; private panelPartView!: ISerializableView; @@ -263,6 +266,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.notificationService = accessor.get(INotificationService); this.activityBarService = accessor.get(IActivityBarService); this.statusBarService = accessor.get(IStatusbarService); + accessor.get(IBannerService); // Listeners this.registerLayoutListeners(); @@ -438,11 +442,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } if (position === Position.LEFT) { - this.workbenchGrid.moveViewTo(this.activityBarPartView, [1, 0]); - this.workbenchGrid.moveViewTo(this.sideBarPartView, [1, 1]); + this.workbenchGrid.moveViewTo(this.activityBarPartView, [2, 0]); + this.workbenchGrid.moveViewTo(this.sideBarPartView, [2, 1]); } else { - this.workbenchGrid.moveViewTo(this.sideBarPartView, [1, 4]); - this.workbenchGrid.moveViewTo(this.activityBarPartView, [1, 4]); + this.workbenchGrid.moveViewTo(this.sideBarPartView, [2, 4]); + this.workbenchGrid.moveViewTo(this.activityBarPartView, [2, 4]); } this.layout(); @@ -597,12 +601,14 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Files to diff is exclusive return pathsToEditors(initialFilesToOpen.filesToDiff, fileService).then(filesToDiff => { if (filesToDiff.length === 2) { - return [{ - leftResource: filesToDiff[0].resource, - rightResource: filesToDiff[1].resource, + const diffEditorInput: IResourceDiffEditorInput[] = [{ + originalInput: { resource: filesToDiff[0].resource }, + modifiedInput: { resource: filesToDiff[1].resource }, options: { pinned: true }, forceFile: true }]; + + return diffEditorInput; } // Otherwise: Open/Create files @@ -697,7 +703,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi let openEditorsPromise: Promise | undefined = undefined; if (editors.length) { - openEditorsPromise = this.editorService.openEditors(editors); + openEditorsPromise = this.editorService.openEditors(editors, undefined, { validateTrust: true }); } // do not block the overall layout ready flow from potentially @@ -900,7 +906,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi case Parts.STATUSBAR_PART: this.statusBarService.focus(); default: - // Title Bar simply pass focus to container + // Title Bar & Banner simply pass focus to container const container = this.getContainer(part); if (container) { container.focus(); @@ -912,6 +918,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi switch (part) { case Parts.TITLEBAR_PART: return this.getPart(Parts.TITLEBAR_PART).getContainer(); + case Parts.BANNER_PART: + return this.getPart(Parts.BANNER_PART).getContainer(); case Parts.ACTIVITYBAR_PART: return this.getPart(Parts.ACTIVITYBAR_PART).getContainer(); case Parts.SIDEBAR_PART: @@ -1044,7 +1052,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi silentNotifications: boolean; } = this.configurationService.getValue('zenMode'); - toggleFullScreen = !this.state.fullscreen && config.fullScreen; + toggleFullScreen = !this.state.fullscreen && config.fullScreen && !isIOS; this.state.zenMode.transitionedToFullScreen = restoring ? config.fullScreen : toggleFullScreen; this.state.zenMode.transitionedToCenteredEditorLayout = !this.isEditorLayoutCentered() && config.centerLayout; @@ -1159,6 +1167,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi protected createWorkbenchLayout(): void { const titleBar = this.getPart(Parts.TITLEBAR_PART); + const bannerPart = this.getPart(Parts.BANNER_PART); const editorPart = this.getPart(Parts.EDITOR_PART); const activityBar = this.getPart(Parts.ACTIVITYBAR_PART); const panelPart = this.getPart(Parts.PANEL_PART); @@ -1167,6 +1176,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // View references for all parts this.titleBarPartView = titleBar; + this.bannerPartView = bannerPart; this.sideBarPartView = sideBar; this.activityBarPartView = activityBar; this.editorPartView = editorPart; @@ -1175,6 +1185,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const viewMap = { [Parts.ACTIVITYBAR_PART]: this.activityBarPartView, + [Parts.BANNER_PART]: this.bannerPartView, [Parts.TITLEBAR_PART]: this.titleBarPartView, [Parts.EDITOR_PART]: this.editorPartView, [Parts.PANEL_PART]: this.panelPartView, @@ -1223,6 +1234,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.storageService.store(Storage.PANEL_SIZE, panelSize, StorageScope.GLOBAL, StorageTarget.MACHINE); this.storageService.store(Storage.PANEL_DIMENSION, positionToString(this.state.panel.position), StorageScope.GLOBAL, StorageTarget.MACHINE); + // Remember last panel size for both dimensions + this.storageService.store(Storage.PANEL_LAST_NON_MAXIMIZED_HEIGHT, this.state.panel.lastNonMaximizedHeight, StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(Storage.PANEL_LAST_NON_MAXIMIZED_WIDTH, this.state.panel.lastNonMaximizedWidth, StorageScope.GLOBAL, StorageTarget.MACHINE); + const gridSize = grid.getViewSize(); this.storageService.store(Storage.GRID_WIDTH, gridSize.width, StorageScope.GLOBAL, StorageTarget.MACHINE); this.storageService.store(Storage.GRID_HEIGHT, gridSize.height, StorageScope.GLOBAL, StorageTarget.MACHINE); @@ -1354,6 +1369,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.workbenchGrid.setViewVisible(this.activityBarPartView, !hidden); } + setBannerHidden(hidden: boolean): void { + this.workbenchGrid.setViewVisible(this.bannerPartView, !hidden); + } + setEditorHidden(hidden: boolean, skipLayout?: boolean): void { this.state.editor.hidden = hidden; @@ -1609,7 +1628,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi let newVisibilityValue: string; if (currentVisibilityValue === 'visible' || currentVisibilityValue === 'classic') { - newVisibilityValue = 'compact'; + newVisibilityValue = getTitleBarStyle(this.configurationService) === 'native' ? 'toggle' : 'compact'; } else { newVisibilityValue = 'classic'; } @@ -1738,6 +1757,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const panelSize = panelDimension === this.state.panel.position ? this.storageService.getNumber(Storage.PANEL_SIZE, StorageScope.GLOBAL, fallbackPanelSize) : fallbackPanelSize; const titleBarHeight = this.titleBarPartView.minimumHeight; + const bannerHeight = this.bannerPartView.minimumHeight; const statusBarHeight = this.statusBarPartView.minimumHeight; const activityBarWidth = this.activityBarPartView.minimumWidth; const middleSectionHeight = height - titleBarHeight - statusBarHeight; @@ -1790,6 +1810,12 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi size: titleBarHeight, visible: this.isVisible(Parts.TITLEBAR_PART) }, + { + type: 'leaf', + data: { type: Parts.BANNER_PART }, + size: bannerHeight, + visible: false + }, { type: 'branch', data: middleSection, diff --git a/src/vs/workbench/browser/media/style.css b/src/vs/workbench/browser/media/style.css index 0b5d4a43b2..4cdda20eaa 100644 --- a/src/vs/workbench/browser/media/style.css +++ b/src/vs/workbench/browser/media/style.css @@ -194,10 +194,12 @@ body.web { .monaco-workbench .select-container:after { content: "\eab4"; font-family: codicon; + /* {{SQL CARBON EDIT}} */ font-size: 16px; width: 16px; height: 16px; line-height: 16px; + /* {{SQL CARBON EDIT}} - End */ position: absolute; top: 0; bottom: 0; diff --git a/src/vs/workbench/browser/panel.ts b/src/vs/workbench/browser/panel.ts index fe7280a97f..7038b86777 100644 --- a/src/vs/workbench/browser/panel.ts +++ b/src/vs/workbench/browser/panel.ts @@ -10,7 +10,7 @@ import { IConstructorSignature0, BrandedService, IInstantiationService } from 'v 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 { CompositeMenuActions } from 'vs/workbench/browser/actions'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IStorageService } from 'vs/platform/storage/common/storage'; diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 5374490bce..a197f847fd 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -172,17 +172,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { // Menu const menuBarVisibility = getMenuBarVisibility(this.configurationService); if (menuBarVisibility === 'compact' || menuBarVisibility === 'hidden' || menuBarVisibility === 'toggle') { - topActions.push({ - id: 'toggleMenuVisibility', - label: localize('menu', "Menu"), - class: undefined, - tooltip: localize('menu', "Menu"), - checked: menuBarVisibility === 'compact', - enabled: true, - expanded: false, - run: async () => this.configurationService.updateValue('window.menuBarVisibility', menuBarVisibility === 'compact' ? 'toggle' : 'compact'), - dispose: () => { } - }); + topActions.push(toAction({ id: 'toggleMenuVisibility', label: localize('menu', "Menu"), checked: menuBarVisibility === 'compact', run: () => this.configurationService.updateValue('window.menuBarVisibility', menuBarVisibility === 'compact' ? 'toggle' : 'compact') })); } if (topActions.length) { @@ -191,25 +181,14 @@ export class ActivitybarPart extends Part implements IActivityBarService { // Accounts 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(toAction({ id: 'toggleAccountsVisibility', label: localize('accounts', "Accounts"), checked: this.accountsVisibilityPreference, run: () => this.accountsVisibilityPreference = !this.accountsVisibilityPreference })); actions.push(new Separator()); // Toggle Sidebar - actions.push(this.instantiationService.createInstance(ToggleSidebarPositionAction, ToggleSidebarPositionAction.ID, ToggleSidebarPositionAction.getLabel(this.layoutService))); + actions.push(toAction({ id: ToggleSidebarPositionAction.ID, label: ToggleSidebarPositionAction.getLabel(this.layoutService), run: () => this.instantiationService.invokeFunction(accessor => new ToggleSidebarPositionAction().run(accessor)) })); // Toggle Activity Bar - actions.push(toAction({ id: ToggleActivityBarVisibilityAction.ID, label: localize('hideActivitBar', "Hide Activity Bar"), run: async () => this.instantiationService.invokeFunction(accessor => new ToggleActivityBarVisibilityAction().run(accessor)) })); + actions.push(toAction({ id: ToggleActivityBarVisibilityAction.ID, label: localize('hideActivitBar', "Hide Activity Bar"), run: () => this.instantiationService.invokeFunction(accessor => new ToggleActivityBarVisibilityAction().run(accessor)) })); }, getContextMenuActionsForComposite: compositeId => this.getContextMenuActionsForComposite(compositeId), getDefaultCompositeId: () => this.viewDescriptorService.getDefaultViewContainer(this.location)!.id, diff --git a/src/vs/workbench/browser/parts/banner/bannerPart.ts b/src/vs/workbench/browser/parts/banner/bannerPart.ts new file mode 100644 index 0000000000..c04b35e865 --- /dev/null +++ b/src/vs/workbench/browser/parts/banner/bannerPart.ts @@ -0,0 +1,331 @@ +/*--------------------------------------------------------------------------------------------- + * 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/bannerpart'; +import { localize } from 'vs/nls'; +import { $, addDisposableListener, append, clearNode, EventType } from 'vs/base/browser/dom'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Codicon, registerCodicon } from 'vs/base/common/codicons'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { Part } from 'vs/workbench/browser/part'; +import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; +import { Action } from 'vs/base/common/actions'; +import { Link } from 'vs/platform/opener/browser/link'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { Emitter } from 'vs/base/common/event'; +import { IBannerItem, IBannerService } from 'vs/workbench/services/banner/browser/bannerService'; +import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; +import { BANNER_BACKGROUND, BANNER_FOREGROUND, BANNER_ICON_FOREGROUND } from 'vs/workbench/common/theme'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { CATEGORIES } from 'vs/workbench/common/actions'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + + +// Icons + +const bannerCloseIcon = registerCodicon('banner-close', Codicon.close); + + +// Theme support + +registerThemingParticipant((theme, collector) => { + const backgroundColor = theme.getColor(BANNER_BACKGROUND); + if (backgroundColor) { + collector.addRule(`.monaco-workbench .part.banner { background-color: ${backgroundColor}; }`); + } + + const foregroundColor = theme.getColor(BANNER_FOREGROUND); + if (foregroundColor) { + collector.addRule(` + .monaco-workbench .part.banner, + .monaco-workbench .part.banner .action-container .codicon, + .monaco-workbench .part.banner .message-actions-container .monaco-link + { color: ${foregroundColor}; } + `); + } + + const iconForegroundColor = theme.getColor(BANNER_ICON_FOREGROUND); + if (iconForegroundColor) { + collector.addRule(`.monaco-workbench .part.banner .icon-container .codicon { color: ${iconForegroundColor} }`); + } +}); + + +// Banner Part + +const CONTEXT_BANNER_FOCUSED = new RawContextKey('bannerFocused', false, localize('bannerFocused', "Whether the banner has keyboard focus")); + +export class BannerPart extends Part implements IBannerService { + declare readonly _serviceBrand: undefined; + + // #region IView + + readonly height: number = 26; + readonly minimumWidth: number = 0; + readonly maximumWidth: number = Number.POSITIVE_INFINITY; + + get minimumHeight(): number { + return this.visible ? this.height : 0; + } + + get maximumHeight(): number { + return this.visible ? this.height : 0; + } + + private _onDidChangeSize = new Emitter<{ width: number; height: number; } | undefined>(); + + override get onDidChange() { return this._onDidChangeSize.event; } + + //#endregion + + private item: IBannerItem | undefined; + private readonly markdownRenderer: MarkdownRenderer; + private visible = false; + + private actionBar: ActionBar | undefined; + private messageActionsContainer: HTMLElement | undefined; + private focusedActionIndex: number = -1; + + constructor( + @IThemeService themeService: IThemeService, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, + @IStorageService storageService: IStorageService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(Parts.BANNER_PART, { hasTitle: false }, themeService, storageService, layoutService); + + this.markdownRenderer = this.instantiationService.createInstance(MarkdownRenderer, {}); + } + + override createContentArea(parent: HTMLElement): HTMLElement { + this.element = parent; + this.element.tabIndex = 0; + + // Restore focused action if needed + this._register(addDisposableListener(this.element, EventType.FOCUS, () => { + if (this.focusedActionIndex !== -1) { + this.focusActionLink(); + } + })); + + // Track focus + const scopedContextKeyService = this.contextKeyService.createScoped(this.element); + CONTEXT_BANNER_FOCUSED.bindTo(scopedContextKeyService).set(true); + + return this.element; + } + + private close(item: IBannerItem): void { + // Hide banner + this.setVisibility(false); + + // Remove from document + clearNode(this.element); + + // Remember choice + if (typeof item.onClose === 'function') { + item.onClose(); + } + + this.item = undefined; + } + + private focusActionLink(): void { + const length = this.item?.actions?.length ?? 0; + + if (this.focusedActionIndex < length) { + const actionLink = this.messageActionsContainer?.children[this.focusedActionIndex]; + if (actionLink instanceof HTMLElement) { + this.actionBar?.setFocusable(false); + actionLink.focus(); + } + } else { + this.actionBar?.focus(0); + } + } + + private getAriaLabel(item: IBannerItem): string | undefined { + if (item.ariaLabel) { + return item.ariaLabel; + } + if (typeof item.message === 'string') { + return item.message; + } + + return undefined; + } + + private getBannerMessage(message: MarkdownString | string): HTMLElement { + if (typeof message === 'string') { + const element = $('span'); + element.innerText = message; + return element; + } + + return this.markdownRenderer.render(message).element; + } + + private setVisibility(visible: boolean): void { + if (visible !== this.visible) { + this.visible = visible; + this.focusedActionIndex = -1; + + this.layoutService.setBannerHidden(!visible); + this._onDidChangeSize.fire(undefined); + } + } + + focus(): void { + this.focusedActionIndex = -1; + this.element.focus(); + } + + focusNextAction(): void { + const length = this.item?.actions?.length ?? 0; + this.focusedActionIndex = this.focusedActionIndex < length ? this.focusedActionIndex + 1 : 0; + + this.focusActionLink(); + } + + focusPreviousAction(): void { + const length = this.item?.actions?.length ?? 0; + this.focusedActionIndex = this.focusedActionIndex > 0 ? this.focusedActionIndex - 1 : length; + + this.focusActionLink(); + } + + hide(id: string): void { + if (this.item?.id !== id) { + return; + } + + this.setVisibility(false); + } + + show(item: IBannerItem): void { + if (item.id === this.item?.id) { + this.setVisibility(true); + return; + } + + // Clear previous item + clearNode(this.element); + + // Banner aria label + const ariaLabel = this.getAriaLabel(item); + if (ariaLabel) { + this.element.setAttribute('aria-label', ariaLabel); + } + + // Icon + const iconContainer = append(this.element, $('div.icon-container')); + iconContainer.setAttribute('aria-hidden', 'true'); + iconContainer.appendChild($(`div${item.icon.cssSelector}`)); + + // Message + const messageContainer = append(this.element, $('div.message-container')); + messageContainer.setAttribute('aria-hidden', 'true'); + messageContainer.appendChild(this.getBannerMessage(item.message)); + + // Message Actions + if (item.actions) { + this.messageActionsContainer = append(this.element, $('div.message-actions-container')); + + for (const action of item.actions) { + const actionLink = this._register(this.instantiationService.createInstance(Link, action, {})); + actionLink.el.tabIndex = -1; + actionLink.el.setAttribute('role', 'button'); + this.messageActionsContainer.appendChild(actionLink.el); + } + } + + // Action + const actionBarContainer = append(this.element, $('div.action-container')); + this.actionBar = this._register(new ActionBar(actionBarContainer)); + const closeAction = this._register(new Action('banner.close', 'Close Banner', bannerCloseIcon.classNames, true, () => Promise.resolve(this.close(item)))); // {{SQL CARBON EDIT}} + this.actionBar.push(closeAction, { icon: true, label: false }); + this.actionBar.setFocusable(false); + + this.setVisibility(true); + this.item = item; + } + + toJSON(): object { + return { + type: Parts.BANNER_PART + }; + } +} + +registerSingleton(IBannerService, BannerPart); + + +// Keybindings + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.banner.focusBanner', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Escape, + when: CONTEXT_BANNER_FOCUSED, + handler: (accessor: ServicesAccessor) => { + const bannerService = accessor.get(IBannerService); + bannerService.focus(); + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.banner.focusNextAction', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.RightArrow, + secondary: [KeyCode.DownArrow], + when: CONTEXT_BANNER_FOCUSED, + handler: (accessor: ServicesAccessor) => { + const bannerService = accessor.get(IBannerService); + bannerService.focusNextAction(); + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.banner.focusPreviousAction', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.LeftArrow, + secondary: [KeyCode.UpArrow], + when: CONTEXT_BANNER_FOCUSED, + handler: (accessor: ServicesAccessor) => { + const bannerService = accessor.get(IBannerService); + bannerService.focusPreviousAction(); + } +}); + + +// Actions + +class FocusBannerAction extends Action2 { + + static readonly ID = 'workbench.action.focusBanner'; + static readonly LABEL = localize('focusBanner', "Focus Banner"); + + constructor() { + super({ + id: FocusBannerAction.ID, + title: { value: FocusBannerAction.LABEL, original: 'Focus Banner' }, + category: CATEGORIES.View, + f1: true + }); + } + + async run(accessor: ServicesAccessor): Promise { + const layoutService = accessor.get(IWorkbenchLayoutService); + layoutService.focusPart(Parts.BANNER_PART); + } +} + +registerAction2(FocusBannerAction); diff --git a/src/vs/workbench/browser/parts/banner/media/bannerpart.css b/src/vs/workbench/browser/parts/banner/media/bannerpart.css new file mode 100644 index 0000000000..4ef3f47619 --- /dev/null +++ b/src/vs/workbench/browser/parts/banner/media/bannerpart.css @@ -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. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .part.banner { + box-sizing: border-box; + cursor: default; + width: 100%; + height: 100%; + font-size: 12px; + display: flex; + overflow: visible; +} + +.monaco-workbench .part.banner .icon-container { + display: flex; + align-items: center; + padding: 0 6px 0 10px; +} + +.monaco-workbench .part.banner .message-container { + line-height: 26px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.monaco-workbench .part.banner .message-container p { + margin-block-start: 0; + margin-block-end: 0; +} + +.monaco-workbench .part.banner .message-actions-container { + flex-grow: 1; + flex-shrink: 0; + line-height: 26px; +} + +.monaco-workbench .part.banner .message-actions-container a { + padding: 3px; + margin-left: 12px; + text-decoration: underline; +} + +.monaco-workbench .part.banner .message-actions-container a:hover { + text-decoration: none; +} + +.monaco-workbench .part.banner .action-container { + padding: 0 10px 0 6px; +} diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index 008862d644..ec974119f5 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -22,6 +22,7 @@ import { ViewContainerLocation, IViewDescriptorService } from 'vs/workbench/comm import { IPaneComposite } from 'vs/workbench/common/panecomposite'; import { IComposite } from 'vs/workbench/common/composite'; import { CompositeDragAndDropData, CompositeDragAndDropObserver, IDraggedCompositeData, ICompositeDragAndDrop, Before2D, toggleDropEffect } from 'vs/workbench/browser/dnd'; +import { Gesture, EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; export interface ICompositeBarItem { id: string; @@ -233,6 +234,8 @@ export class CompositeBar extends Widget implements ICompositeBar { // Contextmenu for composites this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, e => this.showContextMenu(e))); + this._register(Gesture.addTarget(parent)); + this._register(addDisposableListener(parent, TouchEventType.Contextmenu, e => this.showContextMenu(e))); let insertDropBefore: Before2D | undefined = undefined; // Register a drop target on the whole bar to prevent forbidden feedback @@ -619,7 +622,7 @@ export class CompositeBar extends Widget implements ICompositeBar { return this.model.visibleItems.filter(c => overflowingIds.includes(c.id)).map(item => { return { id: item.id, name: this.getAction(item.id)?.label || item.name }; }); } - private showContextMenu(e: MouseEvent): void { + private showContextMenu(e: MouseEvent | GestureEvent): void { EventHelper.stop(e, true); const event = new StandardMouseEvent(e); this.contextMenuService.showContextMenu({ diff --git a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts index 90fe9233e3..1545e7cc5d 100644 --- a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts +++ b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts @@ -71,7 +71,7 @@ export class BrowserDialogHandler implements IDialogHandler { return (severity === Severity.Info) ? 'question' : (severity === Severity.Error) ? 'error' : (severity === Severity.Warning) ? 'warning' : 'none'; } - async show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise { + async show(severity: Severity, message: string, buttons?: string[], options?: IDialogOptions): Promise { this.logService.trace('DialogService#show', message); const result = await this.doShow(this.getDialogType(severity), message, buttons, options?.detail, options?.cancelId, options?.checkbox, undefined, typeof options?.custom === 'object' ? options.custom : undefined); @@ -82,7 +82,7 @@ export class BrowserDialogHandler implements IDialogHandler { }; } - private async doShow(type: 'none' | 'info' | 'error' | 'question' | 'warning' | 'pending' | undefined, message: string, buttons: string[], detail?: string, cancelId?: number, checkbox?: ICheckbox, inputs?: IInput[], customOptions?: ICustomDialogOptions): Promise { + private async doShow(type: 'none' | 'info' | 'error' | 'question' | 'warning' | 'pending' | undefined, message: string, buttons?: string[], detail?: string, cancelId?: number, checkbox?: ICheckbox, inputs?: IInput[], customOptions?: ICustomDialogOptions): Promise { const dialogDisposables = new DisposableStore(); const renderBody = customOptions ? (parent: HTMLElement) => { diff --git a/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index e575c15e3c..9d49a3c848 100644 --- a/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -6,7 +6,8 @@ import 'vs/css!./media/binaryeditor'; import { localize } from 'vs/nls'; import { Emitter } from 'vs/base/common/event'; -import { EditorInput, EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -19,9 +20,10 @@ import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/ import { IStorageService } from 'vs/platform/storage/common/storage'; import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; import { ByteSize } from 'vs/platform/files/common/files'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; export interface IOpenCallbacks { - openInternal: (input: EditorInput, options: EditorOptions | undefined) => Promise; + openInternal: (input: EditorInput, options: IEditorOptions | undefined) => Promise; } /* @@ -70,7 +72,7 @@ export abstract class BaseBinaryResourceEditor extends EditorPane { parent.appendChild(this.scrollbar.getDomNode()); } - override async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override async setInput(input: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { await super.setInput(input, options, context, token); const model = await input.resolve(); @@ -88,7 +90,7 @@ export abstract class BaseBinaryResourceEditor extends EditorPane { this.inputDisposable.value = this.renderInput(input, options, model); } - private renderInput(input: EditorInput, options: EditorOptions | undefined, model: BinaryEditorModel): IDisposable { + private renderInput(input: EditorInput, options: IEditorOptions | undefined, model: BinaryEditorModel): IDisposable { const [binaryContainer, scrollbar] = assertAllDefined(this.binaryContainer, this.scrollbar); clearNode(binaryContainer); diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index ddbc63d96d..ab9220462d 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -24,7 +24,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; 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 { ColorIdentifier, ColorTransform } from 'vs/platform/theme/common/colorRegistry'; import { attachBreadcrumbsStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ResourceLabel } from 'vs/workbench/browser/labels'; @@ -146,7 +146,7 @@ export interface IBreadcrumbsControlOptions { showFileIcons: boolean; showSymbolIcons: boolean; showDecorationColors: boolean; - breadcrumbsBackground: ColorIdentifier | ColorFunction; + breadcrumbsBackground: ColorIdentifier | ColorTransform; showPlaceholder: boolean; } @@ -219,6 +219,7 @@ export class BreadcrumbsControl { this._ckBreadcrumbsActive = BreadcrumbsControl.CK_BreadcrumbsActive.bindTo(this._contextKeyService); this._disposables.add(breadcrumbsService.register(this._editorGroup.id, this._widget)); + this.hide(); } dispose(): void { @@ -312,6 +313,7 @@ export class BreadcrumbsControl { this._breadcrumbsDisposables.add(model); this._breadcrumbsDisposables.add(listener); this._breadcrumbsDisposables.add(configListener); + this._breadcrumbsDisposables.add(toDisposable(() => this._widget.setItems([]))); const updateScrollbarSizing = () => { const sizing = this._cfTitleScrollbarSizing.getValue() ?? 'default'; diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 400d1e1031..cdbf743c3d 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -5,17 +5,19 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { localize } from 'vs/nls'; -import { URI, UriComponents } from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IEditorRegistry, EditorDescriptor } from 'vs/workbench/browser/editor'; -import { EditorInput, IEditorInputSerializer, SideBySideEditorInput, IEditorInputFactoryRegistry, TextCompareEditorActiveContext, ActiveEditorPinnedContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorAvailableEditorIdsContext, MultipleEditorGroupsContext, ActiveEditorDirtyContext, EditorExtensions } from 'vs/workbench/common/editor'; +import { + IEditorInputFactoryRegistry, TextCompareEditorActiveContext, ActiveEditorPinnedContext, EditorExtensions, EditorGroupEditorsCountContext, + ActiveEditorStickyContext, ActiveEditorAvailableEditorIdsContext, MultipleEditorGroupsContext, ActiveEditorDirtyContext +} from 'vs/workbench/common/editor'; +import { SideBySideEditorInput, SideBySideEditorInputSerializer } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { TextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor'; import { SideBySideEditor } from 'vs/workbench/browser/parts/editor/sideBySideEditor'; -import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { DiffEditorInput, DiffEditorInputSerializer } from 'vs/workbench/common/editor/diffEditorInput'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor'; -import { UntitledHintContribution } from 'vs/workbench/browser/parts/editor/untitledHint'; import { BinaryResourceDiffEditor } from 'vs/workbench/browser/parts/editor/binaryDiffEditor'; import { ChangeEncodingAction, ChangeEOLAction, ChangeModeAction, EditorStatus } from 'vs/workbench/browser/parts/editor/editorStatus'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions, CATEGORIES } from 'vs/workbench/common/actions'; @@ -34,37 +36,33 @@ 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, DuplicateGroupDownAction, DuplicateGroupLeftAction, DuplicateGroupRightAction, DuplicateGroupUpAction + QuickAccessPreviousRecentlyUsedEditorAction, OpenPreviousRecentlyUsedEditorInGroupAction, OpenNextRecentlyUsedEditorInGroupAction, QuickAccessLeastRecentlyUsedEditorAction, QuickAccessLeastRecentlyUsedEditorInGroupAction, ReopenResourcesAction, ReOpenInTextEditorAction, DuplicateGroupDownAction, DuplicateGroupLeftAction, DuplicateGroupRightAction, DuplicateGroupUpAction } from 'vs/workbench/browser/parts/editor/editorActions'; import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_EDITORS_TO_THE_RIGHT_COMMAND_ID, CLOSE_EDITOR_COMMAND_ID, CLOSE_EDITOR_GROUP_COMMAND_ID, CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_PINNED_EDITOR_COMMAND_ID, CLOSE_SAVED_EDITORS_COMMAND_ID, GOTO_NEXT_CHANGE, GOTO_PREVIOUS_CHANGE, KEEP_EDITOR_COMMAND_ID, PIN_EDITOR_COMMAND_ID, SHOW_EDITORS_IN_GROUP, SPLIT_EDITOR_DOWN, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, TOGGLE_DIFF_IGNORE_TRIM_WHITESPACE, - TOGGLE_DIFF_SIDE_BY_SIDE, TOGGLE_KEEP_EDITORS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, setup + TOGGLE_DIFF_SIDE_BY_SIDE, TOGGLE_KEEP_EDITORS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, setup as registerEditorCommands } from 'vs/workbench/browser/parts/editor/editorCommands'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { inQuickPickContext, getQuickNavigateHandler } from 'vs/workbench/browser/quickaccess'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; 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/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'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { EditorAutoSave } from 'vs/workbench/browser/parts/editor/editorAutoSave'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; import { ActiveGroupEditorsByMostRecentlyUsedQuickAccess, AllEditorsByAppearanceQuickAccess, AllEditorsByMostRecentlyUsedQuickAccess } from 'vs/workbench/browser/parts/editor/editorQuickAccess'; -import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { FileAccess } from 'vs/base/common/network'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { UntitledTextEditorInputSerializer, UntitledTextEditorWorkingCopyEditorHandler } from 'vs/workbench/services/untitled/common/untitledTextEditorHandler'; + +//#region Editor Registrations -// Register String Editor Registry.as(EditorExtensions.Editors).registerEditor( EditorDescriptor.create( TextResourceEditor, @@ -73,11 +71,10 @@ Registry.as(EditorExtensions.Editors).registerEditor( ), [ new SyncDescriptor(UntitledTextEditorInput), - new SyncDescriptor(ResourceEditorInput) + new SyncDescriptor(TextResourceEditorInput) ] ); -// Register Text Diff Editor Registry.as(EditorExtensions.Editors).registerEditor( EditorDescriptor.create( TextDiffEditor, @@ -89,7 +86,6 @@ Registry.as(EditorExtensions.Editors).registerEditor( ] ); -// Register Binary Resource Diff Editor Registry.as(EditorExtensions.Editors).registerEditor( EditorDescriptor.create( BinaryResourceDiffEditor, @@ -112,186 +108,24 @@ Registry.as(EditorExtensions.Editors).registerEditor( ] ); -interface ISerializedUntitledTextEditorInput { - resourceJSON: UriComponents; - modeId: string | undefined; - encoding: string | undefined; -} - -// Register Editor Input Serializer -class UntitledTextEditorInputSerializer implements IEditorInputSerializer { - - constructor( - @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, - @IPathService private readonly pathService: IPathService - ) { } - - canSerialize(editorInput: EditorInput): boolean { - return this.filesConfigurationService.isHotExitEnabled && !editorInput.isDisposed(); - } - - serialize(editorInput: EditorInput): string | undefined { - if (!this.filesConfigurationService.isHotExitEnabled || editorInput.isDisposed()) { - return undefined; - } - - const untitledTextEditorInput = editorInput as UntitledTextEditorInput; - - let resource = untitledTextEditorInput.resource; - if (untitledTextEditorInput.model.hasAssociatedFilePath) { - resource = toLocalResource(resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme); // untitled with associated file path use the local schema - } - - // Mode: only remember mode if it is either specific (not text) - // or if the mode was explicitly set by the user. We want to preserve - // this information across restarts and not set the mode unless - // this is the case. - let modeId: string | undefined; - const modeIdCandidate = untitledTextEditorInput.getMode(); - if (modeIdCandidate !== PLAINTEXT_MODE_ID) { - modeId = modeIdCandidate; - } else if (untitledTextEditorInput.model.hasModeSetExplicitly) { - modeId = modeIdCandidate; - } - - const serialized: ISerializedUntitledTextEditorInput = { - resourceJSON: resource.toJSON(), - modeId, - encoding: untitledTextEditorInput.getEncoding() - }; - - return JSON.stringify(serialized); - } - - deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): UntitledTextEditorInput { - return instantiationService.invokeFunction(accessor => { - const deserialized: ISerializedUntitledTextEditorInput = JSON.parse(serializedEditorInput); - const resource = URI.revive(deserialized.resourceJSON); - const mode = deserialized.modeId; - const encoding = deserialized.encoding; - - return accessor.get(IEditorService).createEditorInput({ resource, mode, encoding, forceUntitled: true }) as UntitledTextEditorInput; - }); - } -} - Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputSerializer(UntitledTextEditorInput.ID, UntitledTextEditorInputSerializer); - -// Register SideBySide/DiffEditor Input Serializer -interface ISerializedSideBySideEditorInput { - name: string; - description: string | undefined; - - primarySerialized: string; - secondarySerialized: string; - - primaryTypeId: string; - secondaryTypeId: string; -} - -export abstract class AbstractSideBySideEditorInputSerializer implements IEditorInputSerializer { - - private getInputSerializers(secondaryEditorInputTypeId: string, primaryEditorInputTypeId: string): [IEditorInputSerializer | undefined, IEditorInputSerializer | undefined] { - const registry = Registry.as(EditorExtensions.EditorInputFactories); - - return [registry.getEditorInputSerializer(secondaryEditorInputTypeId), registry.getEditorInputSerializer(primaryEditorInputTypeId)]; - } - - canSerialize(editorInput: EditorInput): boolean { - const input = editorInput as SideBySideEditorInput | DiffEditorInput; - - if (input.primary && input.secondary) { - const [secondaryInputSerializer, primaryInputSerializer] = this.getInputSerializers(input.secondary.typeId, input.primary.typeId); - - return !!(secondaryInputSerializer?.canSerialize(input.secondary) && primaryInputSerializer?.canSerialize(input.primary)); - } - - return false; - } - - serialize(editorInput: EditorInput): string | undefined { - const input = editorInput as SideBySideEditorInput | DiffEditorInput; - - if (input.primary && input.secondary) { - const [secondaryInputSerializer, primaryInputSerializer] = this.getInputSerializers(input.secondary.typeId, input.primary.typeId); - if (primaryInputSerializer && secondaryInputSerializer) { - const primarySerialized = primaryInputSerializer.serialize(input.primary); - const secondarySerialized = secondaryInputSerializer.serialize(input.secondary); - - if (primarySerialized && secondarySerialized) { - const serializedEditorInput: ISerializedSideBySideEditorInput = { - name: input.getName(), - description: input.getDescription(), - primarySerialized: primarySerialized, - secondarySerialized: secondarySerialized, - primaryTypeId: input.primary.typeId, - secondaryTypeId: input.secondary.typeId - }; - - return JSON.stringify(serializedEditorInput); - } - } - } - - return undefined; - } - - deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput | undefined { - const deserialized: ISerializedSideBySideEditorInput = JSON.parse(serializedEditorInput); - - const [secondaryInputSerializer, primaryInputSerializer] = this.getInputSerializers(deserialized.secondaryTypeId, deserialized.primaryTypeId); - if (primaryInputSerializer && secondaryInputSerializer) { - const primaryInput = primaryInputSerializer.deserialize(instantiationService, deserialized.primarySerialized); - const secondaryInput = secondaryInputSerializer.deserialize(instantiationService, deserialized.secondarySerialized); - - if (primaryInput && secondaryInput) { - return this.createEditorInput(instantiationService, deserialized.name, deserialized.description, secondaryInput, primaryInput); - } - } - - return undefined; - } - - protected abstract createEditorInput(instantiationService: IInstantiationService, name: string, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput; -} - -class SideBySideEditorInputSerializer extends AbstractSideBySideEditorInputSerializer { - - protected createEditorInput(instantiationService: IInstantiationService, name: string, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput { - return new SideBySideEditorInput(name, description, secondaryInput, primaryInput); - } -} - -class DiffEditorInputSerializer extends AbstractSideBySideEditorInputSerializer { - - protected createEditorInput(instantiationService: IInstantiationService, name: string, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput { - return instantiationService.createInstance(DiffEditorInput, name, description, secondaryInput, primaryInput, undefined); - } -} - Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputSerializer(SideBySideEditorInput.ID, SideBySideEditorInputSerializer); Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputSerializer(DiffEditorInput.ID, DiffEditorInputSerializer); -// Register Editor Contributions +//#endregion + +//#region Workbench Contributions + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(EditorAutoSave, LifecyclePhase.Ready); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(EditorStatus, LifecyclePhase.Ready); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(UntitledTextEditorWorkingCopyEditorHandler, LifecyclePhase.Ready); + registerEditorContribution(OpenWorkspaceButtonContribution.ID, OpenWorkspaceButtonContribution); -// Register Editor Status -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(EditorStatus, LifecyclePhase.Ready); +//#endregion -// Register Editor Auto Save -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(EditorAutoSave, LifecyclePhase.Ready); +//#region Quick Access -// Register Untitled Hint -registerEditorContribution(UntitledHintContribution.ID, UntitledHintContribution); - -// Register Status Actions -const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ChangeModeAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_M) }), 'Change Language Mode', undefined, ContextKeyExpr.not('notebookEditorFocused')); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ChangeEOLAction), 'Change End of Line Sequence'); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ChangeEncodingAction), 'Change File Encoding'); - -// Register Editor Quick Access const quickAccessRegistry = Registry.as(QuickAccessExtensions.Quickaccess); const editorPickerContextKey = 'inEditorsPicker'; const editorPickerContext = ContextKeyExpr.and(inQuickPickContext, ContextKeyExpr.has(editorPickerContextKey)); @@ -320,7 +154,17 @@ quickAccessRegistry.registerQuickAccessProvider({ helpEntries: [{ description: localize('allEditorsByMostRecentlyUsedQuickAccess', "Show All Opened Editors By Most Recently Used"), needsEditor: false }] }); -// Register Editor Actions +//#endregion + +//#region Actions & Commands + +// Editor Status +const registry = Registry.as(ActionExtensions.WorkbenchActions); +registry.registerWorkbenchAction(SyncActionDescriptor.from(ChangeModeAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_M) }), 'Change Language Mode', undefined, ContextKeyExpr.not('notebookEditorFocused')); +registry.registerWorkbenchAction(SyncActionDescriptor.from(ChangeEOLAction), 'Change End of Line Sequence'); +registry.registerWorkbenchAction(SyncActionDescriptor.from(ChangeEncodingAction), 'Change File Encoding'); + +// Editor Management registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenNextEditor, { primary: KeyMod.CtrlCmd | KeyCode.PageDown, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.RightArrow, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_CLOSE_SQUARE_BRACKET] } }), 'View: Open Next Editor', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenPreviousEditor, { primary: KeyMod.CtrlCmd | KeyCode.PageUp, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.LeftArrow, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_OPEN_SQUARE_BRACKET] } }), 'View: Open Previous Editor', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenNextEditorInGroup, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.PageDown), mac: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.RightArrow) } }), 'View: Open Next Editor in Group', CATEGORIES.View.value); @@ -400,16 +244,11 @@ registry.registerWorkbenchAction(SyncActionDescriptor.from(EditorLayoutTwoByTwoG registry.registerWorkbenchAction(SyncActionDescriptor.from(EditorLayoutTwoRowsRightAction), 'View: Two Rows Right Editor Layout', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(EditorLayoutTwoColumnsBottomAction), 'View: Two Columns Bottom Editor Layout', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(ReopenResourcesAction), 'View: Reopen Editor With...', CATEGORIES.View.value, ActiveEditorAvailableEditorIdsContext); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleEditorTypeAction), 'View: Toggle Editor Type', CATEGORIES.View.value, ActiveEditorAvailableEditorIdsContext); - -// Register Quick Editor Actions including built in quick navigate support for some - +registry.registerWorkbenchAction(SyncActionDescriptor.from(ReOpenInTextEditorAction), 'View: Reopen Editor With Text Editor', CATEGORIES.View.value, ActiveEditorAvailableEditorIdsContext); registry.registerWorkbenchAction(SyncActionDescriptor.from(QuickAccessPreviousRecentlyUsedEditorAction), 'View: Quick Open Previous Recently Used Editor', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(QuickAccessLeastRecentlyUsedEditorAction), 'View: Quick Open Least Recently Used Editor', CATEGORIES.View.value); - registry.registerWorkbenchAction(SyncActionDescriptor.from(QuickAccessPreviousRecentlyUsedEditorInGroupAction, { primary: KeyMod.CtrlCmd | KeyCode.Tab, mac: { primary: KeyMod.WinCtrl | KeyCode.Tab } }), 'View: Quick Open Previous Recently Used Editor in Group', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(QuickAccessLeastRecentlyUsedEditorInGroupAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Tab, mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.Tab } }), 'View: Quick Open Least Recently Used Editor in Group', CATEGORIES.View.value); - registry.registerWorkbenchAction(SyncActionDescriptor.from(QuickAccessPreviousEditorFromHistoryAction), 'Quick Open Previous Editor from History'); const quickAccessNavigateNextInEditorPickerId = 'workbench.action.quickOpenNavigateNextInEditorPicker'; @@ -432,10 +271,13 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.Tab } }); -// Editor Commands -setup(); +registerEditorCommands(); -// Touch Bar +//#endregion Workbench Actions + +//#region Menus + +// macOS: Touchbar if (isMacintosh) { MenuRegistry.appendMenuItem(MenuId.TouchBarContext, { command: { id: NavigateBackwardsAction.ID, title: NavigateBackwardsAction.LABEL, icon: { dark: FileAccess.asFileUri('vs/workbench/browser/parts/editor/media/back-tb.png', require) } }, @@ -605,7 +447,6 @@ const previousChangeIcon = registerIcon('diff-editor-previous-change', Codicon.a const nextChangeIcon = registerIcon('diff-editor-next-change', Codicon.arrowDown, localize('nextChangeIcon', 'Icon for the next change action in the diff editor.')); const toggleWhitespace = registerIcon('diff-editor-toggle-whitespace', Codicon.whitespace, localize('toggleWhitespace', 'Icon for the toggle whitespace action in the diff editor.')); - // Diff Editor Title Menu: Previous Change appendEditorToolItem( { @@ -1026,3 +867,5 @@ MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { submenu: MenuId.MenubarSwitchGroupMenu, order: 2 }); + +//#endregion diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 12847827bc..bc03d123dc 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -3,7 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { GroupIdentifier, IWorkbenchEditorConfiguration, EditorOptions, TextEditorOptions, IEditorInput, IEditorIdentifier, IEditorCloseEvent, IEditorPartOptions, IEditorPartOptionsChangeEvent, EditorInput } from 'vs/workbench/common/editor'; +import { GroupIdentifier, IWorkbenchEditorConfiguration, IEditorInput, IEditorIdentifier, IEditorCloseEvent, IEditorPartOptions, IEditorPartOptionsChangeEvent } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IEditorGroup, GroupDirection, IAddGroupOptions, IMergeGroupOptions, GroupsOrder, GroupsArrangement } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Dimension } from 'vs/base/browser/dom'; @@ -13,6 +14,8 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ISerializableView } from 'vs/base/browser/ui/grid/grid'; import { getIEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorService, IResourceEditorInputType } from 'vs/workbench/services/editor/common/editorService'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; export interface IEditorPartCreationOptions { restorePreviousState: boolean; @@ -129,15 +132,20 @@ export interface IEditorGroupView extends IDisposable, ISerializableView, IEdito relayout(): void; } -export function getActiveTextEditorOptions(group: IEditorGroup, expectedActiveEditor?: IEditorInput, presetOptions?: EditorOptions): EditorOptions { +export function getActiveTextEditorOptions(group: IEditorGroup, expectedActiveEditor?: IEditorInput, presetOptions?: IEditorOptions): ITextEditorOptions { const activeGroupCodeEditor = group.activeEditorPane ? getIEditor(group.activeEditorPane.getControl()) : undefined; if (activeGroupCodeEditor) { if (!expectedActiveEditor || expectedActiveEditor.matches(group.activeEditor)) { - return TextEditorOptions.fromEditor(activeGroupCodeEditor, presetOptions); + const textOptions: ITextEditorOptions = { + ...presetOptions, + viewState: withNullAsUndefined(activeGroupCodeEditor.saveViewState()) + }; + + return textOptions; } } - return presetOptions || new EditorOptions(); + return presetOptions || Object.create(null); } /** diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index ab6212159e..75f4ca90c6 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -5,7 +5,8 @@ import { localize } from 'vs/nls'; import { Action } from 'vs/base/common/actions'; -import { IEditorInput, IEditorIdentifier, IEditorCommandsContext, CloseDirection, SaveReason, EditorsOrder, SideBySideEditorInput } from 'vs/workbench/common/editor'; +import { IEditorInput, IEditorIdentifier, IEditorCommandsContext, CloseDirection, SaveReason, EditorsOrder, EditorInputCapabilities, IEditorInputFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -24,6 +25,8 @@ import { Codicon } from 'vs/base/common/codicons'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { EditorOverride } from 'vs/platform/editor/common/editor'; import { Schemas } from 'vs/base/common/network'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/services/editor/common/editorOverrideService'; export class ExecuteCommandAction extends Action { @@ -592,7 +595,7 @@ abstract class BaseCloseAllAction extends Action { // Auto-save on focus change: assume to Save unless the editor is untitled // because bringing up a dialog would save in this case anyway. - if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.ON_FOCUS_CHANGE && !editor.isUntitled()) { + if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.ON_FOCUS_CHANGE && !editor.hasCapability(EditorInputCapabilities.Untitled)) { dirtyEditorsToAutoSave.add(editor); } @@ -1925,10 +1928,12 @@ export class ReopenResourcesAction extends Action { } } -export class ToggleEditorTypeAction extends Action { +export class ReOpenInTextEditorAction extends Action { - static readonly ID = 'workbench.action.toggleEditorType'; - static readonly LABEL = localize('workbench.action.toggleEditorType', "Toggle Editor Type"); + static readonly ID = 'workbench.action.reopenTextEditor'; + static readonly LABEL = localize('workbench.action.reopenTextEditor', "Reopen Editor With Text Editor"); + + private readonly fileEditorInputFactory = Registry.as(EditorExtensions.EditorInputFactories).getFileEditorInputFactory(); constructor( id: string, @@ -1952,12 +1957,17 @@ export class ToggleEditorTypeAction extends Action { const options = activeEditorPane.options; const group = activeEditorPane.group; - const overrides = this.editorService.getEditorOverrides(activeEditorResource, options, group); - const firstNonActiveOverride = overrides.find(([_, entry]) => !entry.active); - if (!firstNonActiveOverride) { + if (this.fileEditorInputFactory.isFileEditorInput(this.editorService.activeEditor)) { return; } - await firstNonActiveOverride[0].open(activeEditorPane.input, { ...options, override: firstNonActiveOverride[1].id }, group)?.override; + // Replace the current editor with the text editor + await this.editorService.replaceEditors([ + { + editor: activeEditorPane.input, + replacement: activeEditorPane.input, + options: { ...options, override: DEFAULT_EDITOR_ASSOCIATION.id }, + } + ], group); } } diff --git a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts index b1e5ee9cc9..ad2c78174c 100644 --- a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts +++ b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts @@ -7,7 +7,7 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Disposable, DisposableStore, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { IFilesConfigurationService, AutoSaveMode, IAutoSaveConfiguration } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { SaveReason, IEditorIdentifier, IEditorInput, GroupIdentifier, ISaveOptions } from 'vs/workbench/common/editor'; +import { SaveReason, IEditorIdentifier, IEditorInput, GroupIdentifier, ISaveOptions, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { withNullAsUndefined } from 'vs/base/common/types'; @@ -90,7 +90,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution } private maybeTriggerAutoSave(reason: SaveReason, editorIdentifier?: IEditorIdentifier): void { - if (editorIdentifier && (editorIdentifier.editor.isReadonly() || editorIdentifier.editor.isUntitled())) { + if (editorIdentifier?.editor.hasCapability(EditorInputCapabilities.Readonly) || editorIdentifier?.editor.hasCapability(EditorInputCapabilities.Untitled)) { return; // no auto save for readonly or untitled editors } diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index ee57645c20..5cc1f03421 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { isObject, isString, isUndefined, isNumber, withNullAsUndefined } from 'vs/base/common/types'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { TextCompareEditorVisibleContext, EditorInput, IEditorIdentifier, IEditorCommandsContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, CloseDirection, IEditorInput, IVisibleEditorPane, ActiveEditorStickyContext, EditorsOrder, viewColumnToEditorGroup, EditorGroupColumn } from 'vs/workbench/common/editor'; +import { TextCompareEditorVisibleContext, IEditorIdentifier, IEditorCommandsContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, CloseDirection, IEditorInput, IVisibleEditorPane, ActiveEditorStickyContext, EditorsOrder, viewColumnToEditorGroup, EditorGroupColumn, EditorInputCapabilities, isEditorIdentifier } from 'vs/workbench/common/editor'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor'; @@ -452,7 +452,7 @@ function registerOpenEditorAPICommands(): void { } }); - CommandsRegistry.registerCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, async function (accessor: ServicesAccessor, leftResource: UriComponents, rightResource: UriComponents, label?: string, columnAndOptions?: [EditorGroupColumn?, ITextEditorOptions?], context?: IOpenEvent) { + CommandsRegistry.registerCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, async function (accessor: ServicesAccessor, originalResource: UriComponents, modifiedResource: UriComponents, label?: string, columnAndOptions?: [EditorGroupColumn?, ITextEditorOptions?], context?: IOpenEvent) { const editorService = accessor.get(IEditorService); const editorGroupService = accessor.get(IEditorGroupsService); @@ -460,8 +460,8 @@ function registerOpenEditorAPICommands(): void { const [options, column] = mixinContext(context, optionsArg, columnArg); await editorService.openEditor({ - leftResource: URI.revive(leftResource), - rightResource: URI.revive(rightResource), + originalInput: { resource: URI.revive(originalResource) }, + modifiedInput: { resource: URI.revive(modifiedResource) }, label, options }, viewColumnToEditorGroup(editorGroupService, column)); @@ -487,7 +487,7 @@ function registerOpenEditorAPICommands(): void { group = editorGroupsService.getGroup(viewColumnToEditorGroup(editorGroupsService, columnArg)) ?? editorGroupsService.activeGroup; } - return editorService.openEditor({ resource: URI.revive(resource), options: { ...optionsArg, override: id } }, group); + return editorService.openEditor({ resource: URI.revive(resource), options: { ...optionsArg, pinned: true, override: id } }, group); }); } @@ -631,13 +631,14 @@ export function splitEditor(editorGroupService: IEditorGroupsService, direction: editorToCopy = withNullAsUndefined(sourceGroup.activeEditor); } - // Copy the editor to the new group, else move the editor to the new group - if (editorToCopy && (editorToCopy as EditorInput).canSplit()) { + // Copy the editor to the new group, else create an empty group + if (editorToCopy && !editorToCopy.hasCapability(EditorInputCapabilities.Singleton)) { sourceGroup.copyEditor(editorToCopy, newGroup); - // Focus - newGroup.focus(); } + // Focus + newGroup.focus(); + } function registerSplitEditorCommands() { @@ -1045,15 +1046,12 @@ export function getMultiSelectedEditorContexts(editorContext: IEditorCommandsCon } function isEditorGroup(thing: unknown): thing is IEditorGroup { - const group = thing as IEditorGroup; + const group = thing as IEditorGroup | undefined; + if (!group) { + return false; + } - return group && typeof group.id === 'number' && Array.isArray(group.editors); -} - -function isEditorIdentifier(thing: unknown): thing is IEditorIdentifier { - const identifier = thing as IEditorIdentifier; - - return identifier && typeof identifier.groupId === 'number'; + return typeof group.id === 'number' && Array.isArray(group.editors); } export function setup(): void { diff --git a/src/vs/workbench/browser/parts/editor/editorControl.ts b/src/vs/workbench/browser/parts/editor/editorControl.ts index 390c2372b2..11f867fb54 100644 --- a/src/vs/workbench/browser/parts/editor/editorControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorControl.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { EditorExtensions, EditorInput, EditorOptions, IEditorOpenContext, IVisibleEditorPane } from 'vs/workbench/common/editor'; +import { EditorExtensions, EditorInputCapabilities, IEditorOpenContext, IVisibleEditorPane } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { Dimension, show, hide } from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; import { IEditorRegistry, IEditorDescriptor } from 'vs/workbench/browser/editor'; @@ -15,6 +16,9 @@ import { IEditorProgressService, LongRunningOperation } from 'vs/platform/progre import { IEditorGroupView, DEFAULT_EDITOR_MIN_DIMENSIONS, DEFAULT_EDITOR_MAX_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; import { Emitter } from 'vs/base/common/event'; import { assertIsDefined } from 'vs/base/common/types'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; +import { WorkspaceTrustRequiredEditor } from 'vs/workbench/browser/parts/editor/workspaceTrustRequiredEditor'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; export interface IOpenEditorResult { readonly editorPane: EditorPane; @@ -42,31 +46,64 @@ export class EditorControl extends Disposable { private readonly activeEditorPaneDisposables = this._register(new DisposableStore()); private dimension: Dimension | undefined; private readonly editorOperation = this._register(new LongRunningOperation(this.editorProgressService)); + private readonly editorsRegistry = Registry.as(EditorExtensions.Editors); constructor( private parent: HTMLElement, private groupView: IEditorGroupView, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IEditorProgressService private readonly editorProgressService: IEditorProgressService + @IEditorProgressService private readonly editorProgressService: IEditorProgressService, + @IWorkspaceTrustManagementService private readonly workspaceTrustService: IWorkspaceTrustManagementService, ) { super(); + + this.registerListeners(); } - async openEditor(editor: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext): Promise { + private registerListeners(): void { + this._register(this.workspaceTrustService.onDidChangeTrust(() => this.onDidChangeWorkspaceTrust())); + } + + private onDidChangeWorkspaceTrust() { + + // If the active editor pane requires workspace trust + // we need to re-open it anytime trust changes to + // account for it. + // For that we explicitly call into the group-view + // to handle errors properly. + const editor = this._activeEditorPane?.input; + const options = this._activeEditorPane?.options; + if (editor?.hasCapability(EditorInputCapabilities.RequiresTrust)) { + this.groupView.openEditor(editor, options); + } + } + + async openEditor(editor: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext = Object.create(null)): Promise { + + // Editor descriptor + const descriptor = this.getEditorDescriptor(editor); // Editor pane - const descriptor = Registry.as(EditorExtensions.Editors).getEditor(editor); - if (!descriptor) { - throw new Error(`No editor descriptor found for input id ${editor.typeId}`); - } const editorPane = this.doShowEditorPane(descriptor); - // Set input + // Apply input to pane const editorChanged = await this.doSetInput(editorPane, editor, options, context); return { editorPane, editorChanged }; } + private getEditorDescriptor(editor: EditorInput): IEditorDescriptor { + if (editor.hasCapability(EditorInputCapabilities.RequiresTrust) && !this.workspaceTrustService.isWorkpaceTrusted()) { + // Workspace trust: if an editor signals it needs workspace trust + // but the current workspace is untrusted, we fallback to a generic + // editor descriptor to indicate this an do NOT load the registered + // editor. + return WorkspaceTrustRequiredEditor.DESCRIPTOR; + } + + return assertIsDefined(this.editorsRegistry.getEditor(editor)); + } + private doShowEditorPane(descriptor: IEditorDescriptor): EditorPane { // Return early if the currently active editor pane can handle the input @@ -108,7 +145,6 @@ export class EditorControl extends Disposable { if (!editorPane.getContainer()) { const editorPaneContainer = document.createElement('div'); editorPaneContainer.classList.add('editor-instance'); - editorPaneContainer.setAttribute('data-editor-id', descriptor.getId()); editorPane.create(editorPaneContainer); } @@ -147,7 +183,7 @@ export class EditorControl extends Disposable { this._onDidChangeSizeConstraints.fire(undefined); } - private async doSetInput(editorPane: EditorPane, editor: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext): Promise { + private async doSetInput(editorPane: EditorPane, editor: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext): Promise { // If the input did not change, return early and only apply the options // unless the options instruct us to force open it even if it is the same diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index 8d27a4d1d5..7da169c1fb 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/editordroptarget'; -import { LocalSelectionTransfer, DraggedEditorIdentifier, ResourcesDropHandler, DraggedEditorGroupIdentifier, DragAndDropObserver, containsDragType, extractResources } from 'vs/workbench/browser/dnd'; +import { LocalSelectionTransfer, DraggedEditorIdentifier, ResourcesDropHandler, DraggedEditorGroupIdentifier, DragAndDropObserver, containsDragType, extractEditorsDropData } from 'vs/workbench/browser/dnd'; import { addDisposableListener, EventType, EventHelper, isAncestor } from 'vs/base/browser/dom'; import { IEditorGroupsAccessor, IEditorGroupView, getActiveTextEditorOptions } from 'vs/workbench/browser/parts/editor/editor'; import { EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; -import { IEditorIdentifier, EditorInput, EditorOptions } from 'vs/workbench/common/editor'; +import { IEditorIdentifier, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { isMacintosh, isWeb } from 'vs/base/common/platform'; import { GroupDirection, IEditorGroupsService, MergeGroupMode } from 'vs/workbench/services/editor/common/editorGroupsService'; import { toDisposable } from 'vs/base/common/lifecycle'; @@ -18,12 +18,12 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { RunOnceScheduler } from 'vs/base/common/async'; import { DataTransfers } from 'vs/base/browser/dnd'; import { VSBuffer } from 'vs/base/common/buffer'; -import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { URI } from 'vs/base/common/uri'; import { joinPath } from 'vs/base/common/resources'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import Severity from 'vs/base/common/severity'; import { localize } from 'vs/nls'; import { ByteSize } from 'vs/platform/files/common/files'; // {{SQL CARBON EDIT}} @@ -59,7 +59,7 @@ class DropOverlay extends Themable { @IInstantiationService private instantiationService: IInstantiationService, @IFileDialogService private readonly fileDialogService: IFileDialogService, @IEditorService private readonly editorService: IEditorService, - @INotificationService private readonly notificationService: INotificationService, + @IDialogService private readonly dialogService: IDialogService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService ) { super(themeService); @@ -285,10 +285,10 @@ class DropOverlay extends Themable { } // Open in target group - const options = getActiveTextEditorOptions(sourceGroup, draggedEditor.editor, EditorOptions.create({ + const options = getActiveTextEditorOptions(sourceGroup, draggedEditor.editor, { pinned: true, // always pin dropped editor sticky: sourceGroup.isSticky(draggedEditor.editor), // preserve sticky state - })); + }); const copyEditor = this.isCopyOperation(event, draggedEditor); if (!copyEditor) { @@ -317,7 +317,7 @@ class DropOverlay extends Themable { // Skip for very large files because this operation is unbuffered if (file.size > DropOverlay.MAX_FILE_UPLOAD_SIZE) { - this.notificationService.warn(localize('fileTooLarge', "File is too large to open as untitled editor. Please upload it first into the file explorer and then try again.")); + this.dialogService.show(Severity.Warning, localize('fileTooLarge', "File is too large to open as untitled editor. Please upload it first into the file explorer and then try again.")); continue; } @@ -336,18 +336,16 @@ class DropOverlay extends Themable { proposedFilePath = joinPath(defaultFilePath, name); } - // Open as untitled file with the provided contents - const untitledEditor = this.editorService.createEditorInput({ - resource: proposedFilePath, - forceUntitled: true, - contents: VSBuffer.wrap(new Uint8Array(event.target.result)).toString() - }); - if (!targetGroup) { targetGroup = ensureTargetGroup(); } - await targetGroup.openEditor(untitledEditor); + // Open as untitled text file with the provided contents + await this.editorService.openEditor({ + resource: proposedFilePath, + forceUntitled: true, + contents: VSBuffer.wrap(new Uint8Array(event.target.result)).toString() + }, targetGroup.id); } }; } @@ -360,7 +358,7 @@ class DropOverlay extends Themable { const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: true /* open workspace instead of file if dropped */ }); // {{SQL CARBON EDIT}} - const untitledOrFileResources = extractResources(event); + const untitledOrFileResources = extractEditorsDropData(event); if (!untitledOrFileResources.length) { return; } @@ -373,16 +371,12 @@ class DropOverlay extends Themable { return; } - dropHandler.handleDrop(event, () => ensureTargetGroup(), targetGroup => { - if (targetGroup) { - targetGroup.focus(); - } - }); + dropHandler.handleDrop(event, () => ensureTargetGroup(), targetGroup => targetGroup?.focus()); } } private isCopyOperation(e: DragEvent, draggedEditor?: IEditorIdentifier): boolean { - if (draggedEditor?.editor instanceof EditorInput && !draggedEditor.editor.canSplit()) { + if (draggedEditor?.editor.hasCapability(EditorInputCapabilities.Singleton)) { return false; } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 481c944862..ec9cc8d7f7 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -5,7 +5,9 @@ import 'vs/css!./media/editorgroupview'; import { EditorGroupModel, IEditorOpenOptions, EditorCloseEvent, ISerializedEditorGroupModel, isSerializedEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel'; -import { EditorInput, EditorOptions, GroupIdentifier, SideBySideEditorInput, CloseDirection, IEditorCloseEvent, ActiveEditorDirtyContext, IEditorPane, EditorGroupEditorsCountContext, SaveReason, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane, ActiveEditorStickyContext, ActiveEditorPinnedContext, EditorResourceAccessor, IEditorMoveEvent } from 'vs/workbench/common/editor'; +import { GroupIdentifier, CloseDirection, IEditorCloseEvent, ActiveEditorDirtyContext, IEditorPane, EditorGroupEditorsCountContext, SaveReason, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane, ActiveEditorStickyContext, ActiveEditorPinnedContext, EditorResourceAccessor, IEditorMoveEvent, EditorInputCapabilities, IEditorOpenEvent } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { Event, Emitter, Relay } from 'vs/base/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Dimension, trackFocus, addDisposableListener, EventType, EventHelper, findParentWithClass, clearNode, isAncestor, asCSSUrl } from 'vs/base/browser/dom'; @@ -45,7 +47,7 @@ import { hash } from 'vs/base/common/hash'; import { guessMimeTypes } from 'vs/base/common/mime'; import { extname, isEqual } from 'vs/base/common/resources'; import { FileAccess, Schemas } from 'vs/base/common/network'; -import { EditorActivation, EditorOpenContext } from 'vs/platform/editor/common/editor'; +import { EditorActivation, EditorOpenContext, IEditorOptions } from 'vs/platform/editor/common/editor'; import { IDialogService, IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; import { ILogService } from 'vs/platform/log/common/log'; import { Codicon } from 'vs/base/common/codicons'; @@ -101,6 +103,9 @@ export class EditorGroupView extends Themable implements IEditorGroupView { private readonly _onWillMoveEditor = this._register(new Emitter()); readonly onWillMoveEditor = this._onWillMoveEditor.event; + private readonly _onWillOpenEditor = this._register(new Emitter()); + readonly onWillOpenEditor = this._onWillOpenEditor.event; + //#endregion private readonly model: EditorGroupModel; @@ -288,7 +293,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { EventHelper.stop(e); // {{SQL CARBON EDIT}} - Create our own editor input so we open an untitled query editor const queryEditorInput = await this.queryEditorService.newSqlEditor({ connectWithGlobal: true, open: false }); - this.openEditor(queryEditorInput, EditorOptions.create({ pinned: true })); + this.openEditor(queryEditorInput, { pinned: true }); } })); @@ -458,11 +463,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } // Determine editor options - let options: EditorOptions; + let options: IEditorOptions; if (from instanceof EditorGroupView) { options = getActiveTextEditorOptions(from); // if we copy from another group, ensure to copy its active editor viewstate } else { - options = new EditorOptions(); + options = Object.create(null); } const activeEditor = this.model.activeEditor; @@ -501,7 +506,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this._register(this.model.onDidCloseEditor(editor => this.handleOnDidCloseEditor(editor))); this._register(this.model.onWillDisposeEditor(editor => this.onWillDisposeEditor(editor))); this._register(this.model.onDidChangeEditorDirty(editor => this.onDidChangeEditorDirty(editor))); - this._register(this.model.onDidEditorLabelChange(editor => this.onDidEditorLabelChange(editor))); + this._register(this.model.onDidChangeEditorLabel(editor => this.onDidChangeEditorLabel(editor))); + this._register(this.model.onDidChangeEditorCapabilities(editor => this.onDidChangeEditorCapabilities(editor))); // Option Changes this._register(this.accessor.onDidChangeEditorPartOptions(e => this.onDidChangeEditorPartOptions(e))); @@ -689,7 +695,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_DIRTY, editor }); } - private onDidEditorLabelChange(editor: EditorInput): void { + private onDidChangeEditorLabel(editor: EditorInput): void { // Forward to title control this.titleAreaControl.updateEditorLabel(editor); @@ -698,6 +704,15 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_LABEL, editor }); } + private onDidChangeEditorCapabilities(editor: EditorInput): void { + + // Forward to title control + this.titleAreaControl.updateEditorCapabilities(editor); + + // Event + this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_CAPABILITIES, editor }); + } + private onDidVisibilityChange(visible: boolean): void { // Forward to editor control @@ -901,26 +916,18 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region openEditor() - async openEditor(editor: EditorInput, options?: EditorOptions): Promise { - - // Guard against invalid inputs - if (!editor) { - return undefined; - } - - // Proceed with opening - return this.doOpenEditor(editor, options); - } - - private async doOpenEditor(editor: EditorInput, options?: EditorOptions): Promise { + async openEditor(editor: EditorInput, options?: IEditorOptions): Promise { // Guard against invalid inputs. Disposed inputs // should never open because they emit no events // e.g. to indicate dirty changes. - if (editor.isDisposed()) { + if (!editor || editor.isDisposed()) { return undefined; // {{SQL CARBON EDIT}} strict-null-checks } + // Fire the event letting everyone know we are about to open an editor + this._onWillOpenEditor.fire({ editor, groupId: this.id }); + // Determine options const openEditorOptions: IEditorOpenOptions = { index: options ? options.index : undefined, @@ -993,7 +1000,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return showEditorResult; } - private doShowEditor(editor: EditorInput, context: { active: boolean, isNew: boolean }, options?: EditorOptions): Promise { + private doShowEditor(editor: EditorInput, context: { active: boolean, isNew: boolean }, options?: IEditorOptions): Promise { // Show in editor control if the active editor changed let openEditorPromise: Promise; @@ -1026,11 +1033,14 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return openEditorPromise; } - private async doHandleOpenEditorError(error: Error, editor: EditorInput, options?: EditorOptions): Promise { + private async doHandleOpenEditorError(error: Error, editor: EditorInput, options?: IEditorOptions): Promise { // Report error only if we are not told to ignore errors that occur from opening an editor if (!isPromiseCanceledError(error) && (!options || !options.ignoreError)) { + // Always log the error to figure out what is going on + this.logService.error(error); + // Since it is more likely that errors fail to open when restoring them e.g. // because files got deleted or moved meanwhile, we do not show any notifications // if we are still restoring editors. @@ -1096,11 +1106,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { Event.once(handle.onDidClose)(() => actions.primary && dispose(actions.primary)); } } - - // Restoring: just log errors to console - else { - this.logService.error(error); - } } // Event @@ -1117,7 +1122,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region openEditors() - async openEditors(editors: { editor: EditorInput, options?: EditorOptions }[]): Promise { + async openEditors(editors: { editor: EditorInput, options?: IEditorOptions }[]): Promise { if (!editors.length) { return null; } @@ -1132,7 +1137,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Open the other ones inactive const startingIndex = this.getIndexOfEditor(editor) + 1; await Promises.settled(editors.map(async ({ editor, options }, index) => { - const adjustedEditorOptions = options || new EditorOptions(); + const adjustedEditorOptions = options || Object.create(null); adjustedEditorOptions.inactive = true; adjustedEditorOptions.pinned = true; adjustedEditorOptions.index = startingIndex + index; @@ -1150,7 +1155,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region moveEditor() - moveEditor(editor: EditorInput, target: IEditorGroupView, options?: EditorOptions): void { + moveEditor(editor: EditorInput, target: IEditorGroupView, options?: IEditorOptions): void { // Move within same group if (this === target) { @@ -1199,11 +1204,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // When moving/copying an editor, try to preserve as much view state as possible // by checking for the editor to be a text editor and creating the options accordingly // if so - const options = getActiveTextEditorOptions(this, editor, EditorOptions.create({ + const options = getActiveTextEditorOptions(this, editor, { ...openOptions, pinned: true, // always pin moved editor sticky: !keepCopy && this.model.isSticky(editor) // preserve sticky state only if editor is moved (https://github.com/microsoft/vscode/issues/99035) - })); + }); // Indicate will move event if (!keepCopy) { @@ -1227,7 +1232,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region copyEditor() - copyEditor(editor: EditorInput, target: IEditorGroupView, options?: EditorOptions): void { + copyEditor(editor: EditorInput, target: IEditorGroupView, options?: IEditorOptions): void { // Move within same group because we do not support to show the same editor // multiple times in the same group @@ -1325,16 +1330,16 @@ export class EditorGroupView extends Themable implements IEditorGroupView { activation = EditorActivation.PRESERVE; } - const options = EditorOptions.create({ preserveFocus, activation }); - - // When closing an editor due to an error we can end up in a loop where we continue closing - // editors that fail to open (e.g. when the file no longer exists). We do not want to show - // repeated errors in this case to the user. As such, if we open the next editor and we are - // in a scope of a previous editor failing, we silence the input errors until the editor is - // opened by setting ignoreError: true. - if (fromError) { - options.ignoreError = true; - } + const options: IEditorOptions = { + preserveFocus, + activation, + // When closing an editor due to an error we can end up in a loop where we continue closing + // editors that fail to open (e.g. when the file no longer exists). We do not want to show + // repeated errors in this case to the user. As such, if we open the next editor and we are + // in a scope of a previous editor failing, we silence the input errors until the editor is + // opened by setting ignoreError: true. + ignoreError: fromError + }; this.openEditor(nextActiveEditor, options); } @@ -1451,7 +1456,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { let confirmation: ConfirmResult; let saveReason = SaveReason.EXPLICIT; let autoSave = false; - if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.ON_FOCUS_CHANGE && !editor.isUntitled() && !options?.skipAutoSave) { + if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.ON_FOCUS_CHANGE && !editor.hasCapability(EditorInputCapabilities.Untitled) && !options?.skipAutoSave) { autoSave = true; confirmation = ConfirmResult.SAVE; saveReason = SaveReason.FOCUS_CHANGE; @@ -1657,7 +1662,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { if (options) { options.index = index; } else { - options = EditorOptions.create({ index }); + options = { index }; } options.inactive = !isActiveEditor; @@ -1676,7 +1681,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { for (const { editor, replacement, forceReplaceDirty, options } of inactiveReplacements) { // Open inactive editor - await this.openEditor(replacement, options); // {{SQL CARBON EDIT}} use this.openEditor to allow us to override the open, we could potentially add this to vscode but i don't think they would care + await this.openEditor(replacement, options); // Close replaced inactive editor unless they match if (!editor.matches(replacement)) { @@ -1697,7 +1702,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { if (activeReplacement) { // Open replacement as active editor - const openEditorResult = this.openEditor(activeReplacement.replacement, activeReplacement.options); // {{SQL CARBON EDIT}} use this.openEditor to allow us to override the open, we could potentially add this to vscode but i don't think they would care + const openEditorResult = this.openEditor(activeReplacement.replacement, activeReplacement.options); // Close replaced active editor unless they match if (!activeReplacement.editor.matches(activeReplacement.replacement)) { @@ -1799,7 +1804,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { export interface EditorReplacement extends IEditorReplacement { readonly editor: EditorInput; readonly replacement: EditorInput; - readonly options?: EditorOptions; + readonly options?: IEditorOptions; } registerThemingParticipant((theme, collector) => { diff --git a/src/vs/workbench/browser/parts/editor/editorPane.ts b/src/vs/workbench/browser/parts/editor/editorPane.ts index 4092712864..1ac5254089 100644 --- a/src/vs/workbench/browser/parts/editor/editorPane.ts +++ b/src/vs/workbench/browser/parts/editor/editorPane.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Composite } from 'vs/workbench/browser/composite'; -import { EditorInput, EditorOptions, IEditorPane, GroupIdentifier, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorPane, GroupIdentifier, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -20,6 +21,7 @@ import { joinPath, IExtUri, isEqual } from 'vs/base/common/resources'; import { indexOfPath } from 'vs/base/common/extpath'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; /** * The base class of editors in the workbench. Editors register themselves for specific editor inputs. @@ -56,8 +58,8 @@ export abstract class EditorPane extends Composite implements IEditorPane { protected _input: EditorInput | undefined; get input(): EditorInput | undefined { return this._input; } - protected _options: EditorOptions | undefined; - get options(): EditorOptions | undefined { return this._options; } + protected _options: IEditorOptions | undefined; + get options(): IEditorOptions | undefined { return this._options; } private _group: IEditorGroup | undefined; get group(): IEditorGroup | undefined { return this._group; } @@ -102,7 +104,7 @@ export abstract class EditorPane extends Composite implements IEditorPane { * The provided cancellation token should be used to test if the operation * was cancelled. */ - async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + async setInput(input: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { this._input = input; this._options = options; } @@ -129,7 +131,7 @@ export abstract class EditorPane extends Composite implements IEditorPane { * Sets the given options to the editor. Clients should apply the options * to the current input. */ - setOptions(options: EditorOptions | undefined): void { + setOptions(options: IEditorOptions | undefined): void { this._options = options; } @@ -193,7 +195,7 @@ export class EditorMemento implements IEditorMemento { private editorDisposables: Map | undefined; constructor( - public readonly id: string, + readonly id: string, private key: string, private memento: MementoObject, private limit: number, @@ -229,7 +231,7 @@ export class EditorMemento implements IEditorMemento { loadEditorState(group: IEditorGroup, resourceOrEditor: URI | EditorInput): T | undefined { const resource = this.doGetResource(resourceOrEditor); if (!resource || !group) { - return undefined; // we are not in a good state to load any state for a resource + return undefined; // we are not in a good state to load any state for a resource {{SQL CARBON EDIT}} Strict nulls } const cache = this.doLoad(); @@ -239,7 +241,7 @@ export class EditorMemento implements IEditorMemento { return mementoForResource[group.id]; } - return undefined; + return undefined; // {{SQL CARBON EDIT}} Strict nulls } clearEditorState(resource: URI, group?: IEditorGroup): void; diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 51bb689be2..6d38d10676 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -211,11 +211,6 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro private whenRestoredResolve: (() => void) | undefined; readonly whenRestored = new Promise(resolve => (this.whenRestoredResolve = resolve)); - private restored = false; - - isRestored(): boolean { - return this.restored; - } get hasRestorableState(): boolean { return !!this.workspaceMemento[EditorPart.EDITOR_PART_UI_STATE_STORAGE_KEY]; @@ -845,7 +840,6 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro // Signal restored Promises.settled(this.groups.map(group => group.whenRestored)).finally(() => { - this.restored = true; this.whenRestoredResolve?.(); }); diff --git a/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts b/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts index ecca0dc199..2abd68f683 100644 --- a/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts +++ b/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts @@ -68,7 +68,7 @@ export abstract class BaseEditorQuickAccessProvider extends PickerQuickAccessPro return super.provide(picker, token); } - protected getPicks(filter: string): Array { + protected _getPicks(filter: string): Array { const query = prepareQuery(filter); // Filtering diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 5cfdf9a84c..2e46e88e6d 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -13,7 +13,7 @@ import { URI } from 'vs/base/common/uri'; import { Action, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import { Language } from 'vs/base/common/platform'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -import { IFileEditorInput, EditorResourceAccessor, SideBySideEditorInput, IEditorPane, IEditorInput, SideBySideEditor } from 'vs/workbench/common/editor'; +import { IFileEditorInput, EditorResourceAccessor, IEditorPane, IEditorInput, SideBySideEditor, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { Disposable, MutableDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IEditorAction } from 'vs/editor/common/editorCommon'; import { EndOfLineSequence } from 'vs/editor/common/model'; @@ -53,6 +53,7 @@ import { IMarker, IMarkerService, MarkerSeverity, IMarkerData } from 'vs/platfor import { STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND } from 'vs/workbench/common/theme'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; // {{SQL CARBON EDIT}} import { setMode } from 'sql/workbench/browser/parts/editor/editorStatusModeSelect'; // {{SQL CARBON EDIT}} @@ -372,7 +373,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { return this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]); } - if (this.editorService.activeEditor?.isReadonly()) { + if (this.editorService.activeEditor?.hasCapability(EditorInputCapabilities.Readonly)) { return this.quickInputService.pick([{ label: localize('noWritableCodeEditor', "The active code editor is read-only.") }]); } @@ -407,13 +408,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { if (!this.tabFocusModeElement.value) { const text = localize('tabFocusModeEnabled', "Tab Moves Focus"); this.tabFocusModeElement.value = this.statusbarService.addEntry({ + name: localize('status.editor.tabFocusMode', "Accessibility Mode"), text, ariaLabel: text, 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', localize('status.editor.tabFocusMode', "Accessibility Mode"), StatusbarAlignment.RIGHT, 100.7); + }, 'status.editor.tabFocusMode', StatusbarAlignment.RIGHT, 100.7); } } else { this.tabFocusModeElement.clear(); @@ -425,13 +427,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { if (!this.columnSelectionModeElement.value) { const text = localize('columnSelectionModeEnabled', "Column Selection"); this.columnSelectionModeElement.value = this.statusbarService.addEntry({ + name: localize('status.editor.columnSelectionMode', "Column Selection Mode"), text, ariaLabel: text, 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', localize('status.editor.columnSelectionMode', "Column Selection Mode"), StatusbarAlignment.RIGHT, 100.8); + }, 'status.editor.columnSelectionMode', StatusbarAlignment.RIGHT, 100.8); } } else { this.columnSelectionModeElement.clear(); @@ -443,12 +446,13 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { if (!this.screenRedearModeElement.value) { const text = localize('screenReaderDetected', "Screen Reader Optimized"); this.screenRedearModeElement.value = this.statusbarService.addEntry({ + name: localize('status.editor.screenReaderMode', "Screen Reader Mode"), text, ariaLabel: text, command: 'showEditorScreenReaderNotification', backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND) - }, 'status.editor.screenReaderMode', localize('status.editor.screenReaderMode', "Screen Reader Mode"), StatusbarAlignment.RIGHT, 100.6); + }, 'status.editor.screenReaderMode', StatusbarAlignment.RIGHT, 100.6); } } else { this.screenRedearModeElement.clear(); @@ -462,13 +466,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } const props: IStatusbarEntry = { + name: localize('status.editor.selection', "Editor Selection"), text, ariaLabel: text, tooltip: localize('gotoLine', "Go to Line/Column"), command: 'workbench.action.gotoLine' }; - this.updateElement(this.selectionElement, props, 'status.editor.selection', localize('status.editor.selection', "Editor Selection"), StatusbarAlignment.RIGHT, 100.5); + this.updateElement(this.selectionElement, props, 'status.editor.selection', StatusbarAlignment.RIGHT, 100.5); } private updateIndentationElement(text: string | undefined): void { @@ -478,13 +483,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } const props: IStatusbarEntry = { + name: localize('status.editor.indentation', "Editor Indentation"), text, ariaLabel: text, tooltip: localize('selectIndentation', "Select Indentation"), command: 'changeEditorIndentation' }; - this.updateElement(this.indentationElement, props, 'status.editor.indentation', localize('status.editor.indentation', "Editor Indentation"), StatusbarAlignment.RIGHT, 100.4); + this.updateElement(this.indentationElement, props, 'status.editor.indentation', StatusbarAlignment.RIGHT, 100.4); } private updateEncodingElement(text: string | undefined): void { @@ -494,13 +500,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } const props: IStatusbarEntry = { + name: localize('status.editor.encoding', "Editor Encoding"), text, ariaLabel: text, tooltip: localize('selectEncoding', "Select Encoding"), command: 'workbench.action.editor.changeEncoding' }; - this.updateElement(this.encodingElement, props, 'status.editor.encoding', localize('status.editor.encoding', "Editor Encoding"), StatusbarAlignment.RIGHT, 100.3); + this.updateElement(this.encodingElement, props, 'status.editor.encoding', StatusbarAlignment.RIGHT, 100.3); } private updateEOLElement(text: string | undefined): void { @@ -510,13 +517,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } const props: IStatusbarEntry = { + name: localize('status.editor.eol', "Editor End of Line"), text, ariaLabel: text, tooltip: localize('selectEOL', "Select End of Line Sequence"), command: 'workbench.action.editor.changeEOL' }; - this.updateElement(this.eolElement, props, 'status.editor.eol', localize('status.editor.eol', "Editor End of Line"), StatusbarAlignment.RIGHT, 100.2); + this.updateElement(this.eolElement, props, 'status.editor.eol', StatusbarAlignment.RIGHT, 100.2); } private updateModeElement(text: string | undefined): void { @@ -526,13 +534,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } const props: IStatusbarEntry = { + name: localize('status.editor.mode', "Editor Language"), text, ariaLabel: text, tooltip: localize('selectLanguageMode', "Select Language Mode"), command: 'workbench.action.editor.changeLanguageMode' }; - this.updateElement(this.modeElement, props, 'status.editor.mode', localize('status.editor.mode', "Editor Language"), StatusbarAlignment.RIGHT, 100.1); + this.updateElement(this.modeElement, props, 'status.editor.mode', StatusbarAlignment.RIGHT, 100.1); } private updateMetadataElement(text: string | undefined): void { @@ -542,17 +551,18 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } const props: IStatusbarEntry = { + name: localize('status.editor.info', "File Information"), text, ariaLabel: text, tooltip: localize('fileInfo', "File Information") }; - this.updateElement(this.metadataElement, props, 'status.editor.info', localize('status.editor.info', "File Information"), StatusbarAlignment.RIGHT, 100); + this.updateElement(this.metadataElement, props, 'status.editor.info', StatusbarAlignment.RIGHT, 100); } - private updateElement(element: MutableDisposable, props: IStatusbarEntry, id: string, name: string, alignment: StatusbarAlignment, priority: number) { + private updateElement(element: MutableDisposable, props: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priority: number) { if (!element.value) { - element.value = this.statusbarService.addEntry(props, id, name, alignment, priority); + element.value = this.statusbarService.addEntry(props, id, alignment, priority); } else { element.value.update(props); } @@ -926,9 +936,9 @@ 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', localize('currentProblem', "Current Problem"), StatusbarAlignment.LEFT); + this.statusBarEntryAccessor.value = this.statusbarService.addEntry({ name: localize('currentProblem', "Current Problem"), text: '', ariaLabel: '' }, 'statusbar.currentProblem', StatusbarAlignment.LEFT); } - this.statusBarEntryAccessor.value.update({ text, ariaLabel: text }); + this.statusBarEntryAccessor.value.update({ name: localize('currentProblem', "Current Problem"), text, ariaLabel: text }); } else { this.statusBarEntryAccessor.clear(); } @@ -1273,7 +1283,7 @@ export class ChangeEOLAction extends Action { return; } - if (this.editorService.activeEditor?.isReadonly()) { + if (this.editorService.activeEditor?.hasCapability(EditorInputCapabilities.Readonly)) { await this.quickInputService.pick([{ label: localize('noWritableCodeEditor', "The active code editor is read-only.") }]); return; } @@ -1290,7 +1300,7 @@ export class ChangeEOLAction extends Action { 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()) { + if (activeCodeEditor?.hasModel() && !this.editorService.activeEditor?.hasCapability(EditorInputCapabilities.Readonly)) { textModel = activeCodeEditor.getModel(); textModel.pushStackElement(); textModel.pushEOL(eol.eol); @@ -1356,7 +1366,7 @@ export class ChangeEncodingAction extends Action { let action: IQuickPickItem | undefined; if (encodingSupport instanceof UntitledTextEditorInput) { action = saveWithEncodingPick; - } else if (activeEditorPane.input.isReadonly()) { + } else if (activeEditorPane.input.hasCapability(EditorInputCapabilities.Readonly)) { action = reopenWithEncodingPick; } else { action = await this.quickInputService.pick([reopenWithEncodingPick, saveWithEncodingPick], { placeHolder: localize('pickAction', "Select Action"), matchOnDetail: true }); diff --git a/src/vs/workbench/browser/parts/editor/editorsObserver.ts b/src/vs/workbench/browser/parts/editor/editorsObserver.ts index 923a4fdb04..c0246cbeb2 100644 --- a/src/vs/workbench/browser/parts/editor/editorsObserver.ts +++ b/src/vs/workbench/browser/parts/editor/editorsObserver.ts @@ -3,7 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEditorInput, IEditorInputFactoryRegistry, IEditorIdentifier, GroupIdentifier, EditorExtensions, IEditorPartOptionsChangeEvent, EditorsOrder, SideBySideEditorInput } from 'vs/workbench/common/editor'; +import { IEditorInput, IEditorInputFactoryRegistry, IEditorIdentifier, GroupIdentifier, EditorExtensions, IEditorPartOptionsChangeEvent, EditorsOrder } from 'vs/workbench/common/editor'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { Registry } from 'vs/platform/registry/common/platform'; diff --git a/src/vs/workbench/browser/parts/editor/media/back-tb.png b/src/vs/workbench/browser/parts/editor/media/back-tb.png index c9d6f935ae4b37dc1dca6f5c14540e0916c715e0..d279d15d18bb8dfc6f06bf5e052ac27433c625d1 100644 GIT binary patch delta 176 zcmeBT`o}mStbVemi(^Oyd7B*sTrO_Z-k@E`{jOo!3l^=WuoVrdf(o$?T@J-$G_cPagMnkryq}6yuYYiZ0=;!MV)4DU&?kU-x9j0IrYxc{uh>a_^aA&cL{NkW%Ay)O9ux$EjG_+8`G z4?Bquzb4qe@nQf08F!Z_doTUV4TbFyM#Pni3*R84h|M*N+UDT;|%Uf}m+AUtwi!SlTyCyB3A@}I{#h<^G o!@FPXS%2s3qQA+EAbYA`Gswuj(Vgt0M{B%-!$7RK;qRn16RRS5tL~U5QoEVk@y~E;g|#HYj4{5UZ-)k#ofE r{CMw<@<&gvMe9H80RR910C=zszdg=-k^pqw00000NkvXXu0mjf;C)zH delta 180 zcmV;l089Ui0)hgNR)3dCL_t(|0qxnb4Z=VSK+zKuFam7Al!_gciY*;P*s)CPppU1PYl+y8 iDf(O@761T%wRr#odtpBS0000T=L diff --git a/src/vs/workbench/browser/parts/editor/media/workspacetrusteditor.css b/src/vs/workbench/browser/parts/editor/media/workspacetrusteditor.css new file mode 100644 index 0000000000..1a4888ecd7 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/media/workspacetrusteditor.css @@ -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. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workspace-trust-required-editor:focus { + outline: none !important; +} + +.monaco-workspace-trust-required-editor { + padding: 5px 0 0 10px; + box-sizing: border-box; +} + +.monaco-workspace-trust-required-editor .embedded-link, +.monaco-workspace-trust-required-editor .embedded-link:hover { + cursor: pointer; + text-decoration: underline; + margin-left: 5px; +} diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index dfec4050bc..fbab21cba4 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -50,7 +50,7 @@ export class NoTabsTitleControl extends TitleControl { this._register(addDisposableListener(this.editorLabel.element, EventType.CLICK, e => this.onTitleLabelClick(e))); // Breadcrumbs - this.createBreadcrumbsControl(labelContainer, { showFileIcons: false, showSymbolIcons: true, showDecorationColors: false, breadcrumbsBackground: () => Color.transparent, showPlaceholder: false }); + this.createBreadcrumbsControl(labelContainer, { showFileIcons: false, showSymbolIcons: true, showDecorationColors: false, breadcrumbsBackground: Color.transparent.toString(), showPlaceholder: false }); titleContainer.classList.toggle('breadcrumbs', Boolean(this.breadcrumbsControl)); this._register(toDisposable(() => titleContainer.classList.remove('breadcrumbs'))); // important to remove because the container is a shared dom node @@ -114,7 +114,7 @@ export class NoTabsTitleControl extends TitleControl { private onTitleTap(e: GestureEvent): void { // We only want to open the quick access picker when - // the tap occured over the editor label, so we need + // the tap occurred over the editor label, so we need // to check on the target // (https://github.com/microsoft/vscode/issues/107543) const target = e.initialTarget; @@ -167,6 +167,10 @@ export class NoTabsTitleControl extends TitleControl { this.ifEditorIsActive(editor, () => this.redraw()); } + updateEditorCapabilities(editor: IEditorInput): void { + this.ifEditorIsActive(editor, () => this.redraw()); + } + updateEditorLabels(): void { if (this.group.activeEditor) { this.updateEditorLabel(this.group.activeEditor); // we only have the active one to update diff --git a/src/vs/workbench/browser/parts/editor/rangeDecorations.ts b/src/vs/workbench/browser/parts/editor/rangeDecorations.ts deleted file mode 100644 index f565ffc1e7..0000000000 --- a/src/vs/workbench/browser/parts/editor/rangeDecorations.ts +++ /dev/null @@ -1,121 +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 { URI } from 'vs/base/common/uri'; -import { Emitter } from 'vs/base/common/event'; -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 { ICodeEditor, isCodeEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser'; -import { TrackedRangeStickiness, IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; -import { isEqual } from 'vs/base/common/resources'; - -export interface IRangeHighlightDecoration { - resource: URI; - range: IRange; - isWholeLine?: boolean; -} - -export class RangeHighlightDecorations extends Disposable { - - private rangeHighlightDecorationId: string | null = null; - private editor: ICodeEditor | null = null; - private readonly editorDisposables = this._register(new DisposableStore()); - - private readonly _onHighlightRemoved: Emitter = this._register(new Emitter()); - readonly onHighlightRemoved = this._onHighlightRemoved.event; - - constructor( - @IEditorService private readonly editorService: IEditorService - ) { - super(); - } - - removeHighlightRange() { - if (this.editor && 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 && 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); - } - - override dispose() { - super.dispose(); - - if (this.editor && this.editor.getModel()) { - this.removeHighlightRange(); - this.editor = null; - } - } -} diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index 1eb57b8777..22d2fe33f8 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -5,7 +5,9 @@ import { Dimension, $, clearNode } from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; -import { EditorInput, EditorOptions, SideBySideEditorInput, IEditorControl, IEditorPane, IEditorOpenContext, EditorExtensions } from 'vs/workbench/common/editor'; +import { IEditorControl, IEditorPane, IEditorOpenContext, EditorExtensions } from 'vs/workbench/common/editor'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -18,6 +20,7 @@ import { SplitView, Sizing, Orientation } from 'vs/base/browser/ui/splitview/spl import { Event, Relay, Emitter } from 'vs/base/common/event'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { assertIsDefined } from 'vs/base/common/types'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; export class SideBySideEditor extends EditorPane { @@ -94,14 +97,14 @@ export class SideBySideEditor extends EditorPane { this.updateStyles(); } - override async setInput(newInput: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { - const oldInput = this.input as SideBySideEditorInput; - await super.setInput(newInput, options, context, token); + override async setInput(input: SideBySideEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + const oldInput = this.input; + await super.setInput(input, options, context, token); - return this.updateInput(oldInput, (newInput as SideBySideEditorInput), options, context, token); + return this.updateInput(oldInput, input, options, context, token); } - override setOptions(options: EditorOptions | undefined): void { + override setOptions(options: IEditorOptions | undefined): void { if (this.primaryEditorPane) { this.primaryEditorPane.setOptions(options); } @@ -162,7 +165,7 @@ export class SideBySideEditor extends EditorPane { return this.secondaryEditorPane; } - private async updateInput(oldInput: SideBySideEditorInput, newInput: SideBySideEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + private async updateInput(oldInput: EditorInput | undefined, newInput: SideBySideEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { if (!newInput.matches(oldInput)) { if (oldInput) { this.disposeEditors(); @@ -181,11 +184,23 @@ export class SideBySideEditor extends EditorPane { ]); } - private setNewInput(newInput: SideBySideEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { - const secondaryEditor = this.doCreateEditor(newInput.secondary, assertIsDefined(this.secondaryEditorContainer)); - const primaryEditor = this.doCreateEditor(newInput.primary, assertIsDefined(this.primaryEditorContainer)); + private async setNewInput(newInput: SideBySideEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + this.secondaryEditorPane = this.doCreateEditor(newInput.secondary, assertIsDefined(this.secondaryEditorContainer)); + this.primaryEditorPane = this.doCreateEditor(newInput.primary, assertIsDefined(this.primaryEditorContainer)); - return this.onEditorsCreated(secondaryEditor, primaryEditor, newInput.secondary, newInput.primary, options, context, token); + this.layout(this.dimension); + + this._onDidChangeSizeConstraints.input = Event.any( + Event.map(this.secondaryEditorPane.onDidChangeSizeConstraints, () => undefined), + Event.map(this.primaryEditorPane.onDidChangeSizeConstraints, () => undefined) + ); + + this.onDidCreateEditors.fire(undefined); + + await Promise.all([ + this.secondaryEditorPane.setInput(newInput.secondary, undefined, context, token), + this.primaryEditorPane.setInput(newInput.primary, options, context, token)] + ); } private doCreateEditor(editorInput: EditorInput, container: HTMLElement): EditorPane { @@ -201,23 +216,6 @@ export class SideBySideEditor extends EditorPane { return editor; } - private async onEditorsCreated(secondary: EditorPane, primary: EditorPane, secondaryInput: EditorInput, primaryInput: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { - this.secondaryEditorPane = secondary; - this.primaryEditorPane = primary; - - this._onDidChangeSizeConstraints.input = Event.any( - Event.map(secondary.onDidChangeSizeConstraints, () => undefined), - Event.map(primary.onDidChangeSizeConstraints, () => undefined) - ); - - this.onDidCreateEditors.fire(undefined); - - await Promise.all([ - this.secondaryEditorPane.setInput(secondaryInput, undefined, context, token), - this.primaryEditorPane.setInput(primaryInput, options, context, token)] - ); - } - override updateStyles(): void { super.updateStyles(); diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 297c566dbc..3ab7c98a53 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/tabstitlecontrol'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { shorten } from 'vs/base/common/labels'; -import { EditorResourceAccessor, GroupIdentifier, IEditorInput, Verbosity, EditorCommandsContextActionRunner, IEditorPartOptions, SideBySideEditor } from 'vs/workbench/common/editor'; +import { EditorResourceAccessor, GroupIdentifier, IEditorInput, Verbosity, IEditorPartOptions, SideBySideEditor } from 'vs/workbench/common/editor'; import { computeEditorAriaLabel } from 'vs/workbench/browser/editor'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/browser/touch'; @@ -19,7 +19,7 @@ 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 { ITitleControlDimensions, TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; +import { EditorCommandsContextActionRunner, 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'; @@ -414,14 +414,8 @@ export class TabsTitleControl extends TitleControl { } closeEditors(editors: IEditorInput[]): void { - // Cleanup closed editors this.handleClosedEditors(); - - // Update Breadcrumbs when last editor closed - if (this.group.count === 0) { - this.breadcrumbsControl?.update(); - } } private handleClosedEditors(): void { @@ -459,6 +453,8 @@ export class TabsTitleControl extends TitleControl { this.tabActionBars = []; this.clearEditorActionsToolbar(); + + this.breadcrumbsControl?.update(); } } @@ -517,6 +513,10 @@ export class TabsTitleControl extends TitleControl { this.layout(this.dimensions); } + updateEditorCapabilities(editor: IEditorInput): void { + this.updateEditorLabel(editor); + } + private updateEditorLabelAggregator = this._register(new RunOnceScheduler(() => this.updateEditorLabels(), 0)); updateEditorLabel(editor: IEditorInput): void { @@ -810,7 +810,7 @@ export class TabsTitleControl extends TitleControl { } // Apply some datatransfer types to allow for dragging the element outside of the application - this.doFillResourceDataTransfers(editor, e); + this.doFillResourceDataTransfers([editor], e); // Fixes https://github.com/microsoft/vscode/issues/18733 tab.classList.add('dragged'); diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 37ea277b5b..f15e841238 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -5,11 +5,12 @@ import { localize } from 'vs/nls'; import { deepClone } from 'vs/base/common/objects'; -import { isFunction, isObject, isArray, assertIsDefined, withUndefinedAsNull } from 'vs/base/common/types'; +import { 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'; -import { TextEditorOptions, EditorInput, EditorOptions, TEXT_DIFF_EDITOR_ID, IEditorInputFactoryRegistry, EditorExtensions, ITextDiffEditorPane, IEditorInput, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { TEXT_DIFF_EDITOR_ID, IEditorInputFactoryRegistry, EditorExtensions, ITextDiffEditorPane, IEditorInput, IEditorOpenContext, EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { DiffNavigator } from 'vs/editor/browser/widget/diffNavigator'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; @@ -21,13 +22,13 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { ScrollType, IDiffEditorViewState, IDiffEditorModel } from 'vs/editor/common/editorCommon'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { URI } from 'vs/base/common/uri'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { EditorActivation, IEditorOptions } from 'vs/platform/editor/common/editor'; +import { EditorActivation, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { isEqual } from 'vs/base/common/resources'; import { multibyteAwareBtoa } from 'vs/base/browser/dom'; @@ -44,6 +45,8 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan private readonly diffNavigatorDisposables = this._register(new DisposableStore()); private reverseColor?: boolean; // {{SQL CARBON EDIT}} add property + private readonly inputListener = this._register(new MutableDisposable()); + override get scopedContextKeyService(): IContextKeyService | undefined { const control = this.getControl(); if (!control) { @@ -69,21 +72,29 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan super(TextDiffEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, editorService, editorGroupService); // Listen to file system provider changes - this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidFileSystemProviderChange(e.scheme))); - this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidFileSystemProviderChange(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidChangeFileSystemProvider(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidChangeFileSystemProvider(e.scheme))); } - private onDidFileSystemProviderChange(scheme: string): void { - const control = this.getControl(); - const input = this.input; + private onDidChangeFileSystemProvider(scheme: string): void { + if (this.input instanceof DiffEditorInput && (this.input.originalInput.resource?.scheme === scheme || this.input.modifiedInput.resource?.scheme === scheme)) { + this.updateReadonly(this.input); + } + } - if (control && input instanceof DiffEditorInput) { - if (input.originalInput.resource?.scheme === scheme || input.modifiedInput.resource?.scheme === scheme) { - control.updateOptions({ - readOnly: input.modifiedInput.isReadonly(), - originalEditable: !input.originalInput.isReadonly() - }); - } + private onDidChangeInputCapabilities(input: DiffEditorInput): void { + if (this.input === input) { + this.updateReadonly(input); + } + } + + private updateReadonly(input: DiffEditorInput): void { + const control = this.getControl(); + if (control) { + control.updateOptions({ + readOnly: input.modifiedInput.hasCapability(EditorInputCapabilities.Readonly), + originalEditable: !input.originalInput.hasCapability(EditorInputCapabilities.Readonly) + }); } } @@ -113,7 +124,10 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan return this.instantiationService.createInstance(DiffEditorWidget, parent, configuration, {}); } - override async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override async setInput(input: DiffEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + + // Update our listener for input capabilities + this.inputListener.value = input.onDidChangeCapabilities(() => this.onDidChangeInputCapabilities(input)); // Dispose previous diff navigator this.diffNavigatorDisposables.clear(); @@ -132,8 +146,9 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan return undefined; } - // Assert Model Instance - if (!(resolvedModel instanceof TextDiffEditorModel) && this.openAsBinary(input, options)) { + // Fallback to open as binary if not text + if (!(resolvedModel instanceof TextDiffEditorModel)) { + this.openAsBinary(input, options); return undefined; } @@ -142,10 +157,10 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan const resolvedDiffEditorModel = resolvedModel as TextDiffEditorModel; diffEditor.setModel(withUndefinedAsNull(resolvedDiffEditorModel.textDiffEditorModel)); - // Apply Options from TextOptions + /// Apply options to editor if any let optionsGotApplied = false; - if (options && isFunction((options).apply)) { - optionsGotApplied = (options).apply(diffEditor, ScrollType.Immediate); + if (options) { + optionsGotApplied = applyTextEditorOptions(options, diffEditor, ScrollType.Immediate); } // Otherwise restore View State unless disabled via settings @@ -172,7 +187,8 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan } catch (error) { // In case we tried to open a file and the response indicates that this is not a text file, fallback to binary diff. - if (this.isFileBinaryError(error) && this.openAsBinary(input, options)) { + if (this.isFileBinaryError(error)) { + this.openAsBinary(input, options); return; } @@ -180,62 +196,51 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan } } - private restoreTextDiffEditorViewState(editor: EditorInput, control: IDiffEditor): boolean { - if (editor instanceof DiffEditorInput) { - const resource = this.toDiffEditorViewStateResource(editor); - if (resource) { - const viewState = this.loadTextEditorViewState(resource); - if (viewState) { - control.restoreViewState(viewState); + private restoreTextDiffEditorViewState(editor: DiffEditorInput, control: IDiffEditor): boolean { + const resource = this.toDiffEditorViewStateResource(editor); + if (resource) { + const viewState = this.loadTextEditorViewState(resource); + if (viewState) { + control.restoreViewState(viewState); - return true; - } + return true; } } return false; } - private openAsBinary(input: EditorInput, options: EditorOptions | undefined): boolean { - if (input instanceof DiffEditorInput) { - const originalInput = input.originalInput; - const modifiedInput = input.modifiedInput; + private openAsBinary(input: DiffEditorInput, options: ITextEditorOptions | undefined): void { + const originalInput = input.originalInput; + const modifiedInput = input.modifiedInput; - const binaryDiffInput = this.instantiationService.createInstance(DiffEditorInput, input.getName(), input.getDescription(), originalInput, modifiedInput, true); + const binaryDiffInput = this.instantiationService.createInstance(DiffEditorInput, input.getName(), input.getDescription(), originalInput, modifiedInput, true); - // Forward binary flag to input if supported - const fileEditorInputFactory = Registry.as(EditorExtensions.EditorInputFactories).getFileEditorInputFactory(); - if (fileEditorInputFactory.isFileEditorInput(originalInput)) { - originalInput.setForceOpenAsBinary(); - } + // Forward binary flag to input if supported + const fileEditorInputFactory = Registry.as(EditorExtensions.EditorInputFactories).getFileEditorInputFactory(); + if (fileEditorInputFactory.isFileEditorInput(originalInput)) { + originalInput.setForceOpenAsBinary(); + } - if (fileEditorInputFactory.isFileEditorInput(modifiedInput)) { - modifiedInput.setForceOpenAsBinary(); - } + if (fileEditorInputFactory.isFileEditorInput(modifiedInput)) { + modifiedInput.setForceOpenAsBinary(); + } - // Make sure to not steal away the currently active group - // because we are triggering another openEditor() call - // and do not control the initial intent that resulted - // in us now opening as binary. - const preservingOptions: IEditorOptions = { + // Replace this editor with the binary one + this.editorService.replaceEditors([{ + editor: input, + replacement: binaryDiffInput, + options: { + ...options, + // Make sure to not steal away the currently active group + // because we are triggering another openEditor() call + // and do not control the initial intent that resulted + // in us now opening as binary. activation: EditorActivation.PRESERVE, pinned: this.group?.isPinned(input), sticky: this.group?.isSticky(input) - }; - - if (options) { - options.overwrite(preservingOptions); - } else { - options = EditorOptions.create(preservingOptions); } - - // Replace this editor with the binary one - this.editorService.replaceEditors([{ editor: input, replacement: binaryDiffInput, options }], this.group || ACTIVE_GROUP); - - return true; - } - - return false; + }], this.group || ACTIVE_GROUP); } protected override computeConfiguration(configuration: IEditorConfiguration): ICodeEditorOptions { @@ -262,8 +267,8 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan protected override getConfigurationOverrides(): ICodeEditorOptions { const options: IDiffEditorOptions = super.getConfigurationOverrides(); - options.readOnly = this.input instanceof DiffEditorInput && this.input.modifiedInput.isReadonly(); - options.originalEditable = this.input instanceof DiffEditorInput && !this.input.originalInput.isReadonly(); + options.readOnly = this.input instanceof DiffEditorInput && this.input.modifiedInput.hasCapability(EditorInputCapabilities.Readonly); + options.originalEditable = this.input instanceof DiffEditorInput && !this.input.originalInput.hasCapability(EditorInputCapabilities.Readonly); options.lineDecorationsWidth = '2ch'; return options; @@ -283,6 +288,9 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan override clearInput(): void { + // Clear input listener + this.inputListener.clear(); + // Dispose previous diff navigator this.diffNavigatorDisposables.clear(); diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index 1c08942c2c..db34217420 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -7,10 +7,12 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { distinct, deepClone } from 'vs/base/common/objects'; import { Event } from 'vs/base/common/event'; -import { isObject, assertIsDefined, withNullAsUndefined, isFunction } from 'vs/base/common/types'; +import { isObject, assertIsDefined, withNullAsUndefined } from 'vs/base/common/types'; import { Dimension } from 'vs/base/browser/dom'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { EditorInput, EditorOptions, IEditorMemento, ITextEditorPane, TextEditorOptions, IEditorCloseEvent, IEditorInput, IEditorOpenContext, EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; +import { IEditorMemento, ITextEditorPane, IEditorCloseEvent, IEditorInput, IEditorOpenContext, EditorResourceAccessor, SideBySideEditor, EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { computeEditorAriaLabel } from 'vs/workbench/browser/editor'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorViewState, IEditor, ScrollType } from 'vs/editor/common/editorCommon'; @@ -19,7 +21,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; -import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; import { isCodeEditor, getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -27,6 +29,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IExtUri } from 'vs/base/common/resources'; import { MutableDisposable } from 'vs/base/common/lifecycle'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; export interface IEditorConfiguration { editor: object; @@ -44,15 +47,11 @@ export abstract class BaseTextEditor extends EditorPane implements ITextEditorPa private editorControl: IEditor | undefined; private editorContainer: HTMLElement | undefined; private hasPendingConfigurationChange: boolean | undefined; - private lastAppliedEditorOptions?: IEditorOptions; + private lastAppliedEditorOptions?: ICodeEditorOptions; private editorMemento: IEditorMemento; private readonly groupListener = this._register(new MutableDisposable()); - private _instantiationService: IInstantiationService; - protected get instantiationService(): IInstantiationService { return this._instantiationService; } - protected set instantiationService(value: IInstantiationService) { this._instantiationService = value; } - override get scopedContextKeyService(): IContextKeyService | undefined { return isCodeEditor(this.editorControl) ? this.editorControl.invokeWithinContext(accessor => accessor.get(IContextKeyService)) : undefined; } @@ -60,7 +59,7 @@ export abstract class BaseTextEditor extends EditorPane implements ITextEditorPa constructor( id: string, @ITelemetryService telemetryService: ITelemetryService, - @IInstantiationService instantiationService: IInstantiationService, + @IInstantiationService protected instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @ITextResourceConfigurationService protected readonly textResourceConfigurationService: ITextResourceConfigurationService, @IThemeService themeService: IThemeService, @@ -69,8 +68,6 @@ export abstract class BaseTextEditor extends EditorPane implements ITextEditorPa ) { super(id, telemetryService, themeService, storageService); - this._instantiationService = instantiationService; - this.editorMemento = this.getEditorMemento(editorGroupService, BaseTextEditor.TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY, 100); this._register(this.textResourceConfigurationService.onDidChangeConfiguration(() => { @@ -105,10 +102,10 @@ export abstract class BaseTextEditor extends EditorPane implements ITextEditorPa } } - protected computeConfiguration(configuration: IEditorConfiguration): IEditorOptions { + protected computeConfiguration(configuration: IEditorConfiguration): ICodeEditorOptions { // Specific editor options always overwrite user configuration - const editorConfiguration: IEditorOptions = isObject(configuration.editor) ? deepClone(configuration.editor) : Object.create(null); + const editorConfiguration: ICodeEditorOptions = isObject(configuration.editor) ? deepClone(configuration.editor) : Object.create(null); Object.assign(editorConfiguration, this.getConfigurationOverrides()); // ARIA label @@ -121,12 +118,12 @@ export abstract class BaseTextEditor extends EditorPane implements ITextEditorPa return this._input ? computeEditorAriaLabel(this._input, undefined, this.group, this.editorGroupService.count) : localize('editor', "Editor"); } - protected getConfigurationOverrides(): IEditorOptions { + protected getConfigurationOverrides(): ICodeEditorOptions { return { overviewRulerLanes: 3, lineNumbersMinChars: 3, fixedOverflowWidgets: true, - readOnly: this.input?.isReadonly(), + readOnly: this.input?.hasCapability(EditorInputCapabilities.Readonly), // render problems even in readonly editors // https://github.com/microsoft/vscode/issues/89057 renderValidationDecorations: 'on' @@ -153,13 +150,13 @@ export abstract class BaseTextEditor extends EditorPane implements ITextEditorPa * * The passed in configuration object should be passed to the editor control when creating it. */ - protected createEditorControl(parent: HTMLElement, configuration: IEditorOptions): IEditor { + protected createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): IEditor { // Use a getter for the instantiation service since some subclasses might use scoped instantiation services return this.instantiationService.createInstance(CodeEditorWidget, parent, configuration, {}); } - override async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override async setInput(input: EditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { await super.setInput(input, options, context, token); // Update editor options after having set the input. We do this because there can be @@ -171,11 +168,9 @@ export abstract class BaseTextEditor extends EditorPane implements ITextEditorPa editorContainer.setAttribute('aria-label', this.computeAriaLabel()); } - override setOptions(options: EditorOptions | undefined): void { - const textOptions = options as TextEditorOptions; - if (textOptions && isFunction(textOptions.apply)) { - const textEditor = assertIsDefined(this.getControl()); - textOptions.apply(textEditor, ScrollType.Smooth); + override setOptions(options: ITextEditorOptions | undefined): void { + if (options) { + applyTextEditorOptions(options, assertIsDefined(this.getControl()), ScrollType.Smooth); } } diff --git a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index 1602b2bb57..d0cbb12f40 100644 --- a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -4,10 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { assertIsDefined, isFunction, withNullAsUndefined } from 'vs/base/common/types'; +import { assertIsDefined, withNullAsUndefined } from 'vs/base/common/types'; import { ICodeEditor, getCodeEditor, IPasteEvent } from 'vs/editor/browser/editorBrowser'; -import { TextEditorOptions, EditorInput, EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; -import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; +import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { AbstractTextResourceEditorInput, TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; @@ -23,8 +25,9 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; -import { EditorOption, IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { EditorOption, IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; import { ModelConstants } from 'vs/editor/common/model'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; /** * An editor implementation that is capable of showing the contents of resource inputs. Uses @@ -53,7 +56,7 @@ export class AbstractTextResourceEditor extends BaseTextEditor { return localize('textEditor', "Text Editor"); } - override async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override async setInput(input: AbstractTextResourceEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { // Remember view settings if input changes this.saveTextResourceEditorViewState(this.input); @@ -77,11 +80,10 @@ export class AbstractTextResourceEditor extends BaseTextEditor { const textEditorModel = resolvedModel.textEditorModel; textEditor.setModel(textEditorModel); - // Apply Options from TextOptions + // Apply options to editor if any let optionsGotApplied = false; - const textOptions = options; - if (textOptions && isFunction(textOptions.apply)) { - optionsGotApplied = textOptions.apply(textEditor, ScrollType.Immediate); + if (options) { + optionsGotApplied = applyTextEditorOptions(options, textEditor, ScrollType.Immediate); } // Otherwise restore View State unless disabled via settings @@ -97,8 +99,8 @@ export class AbstractTextResourceEditor extends BaseTextEditor { textEditor.updateOptions({ readOnly: resolvedModel.isReadonly() }); } - private restoreTextResourceEditorViewState(editor: EditorInput, control: IEditor) { - if (editor instanceof UntitledTextEditorInput || editor instanceof ResourceEditorInput) { + private restoreTextResourceEditorViewState(editor: AbstractTextResourceEditorInput, control: IEditor) { + if (editor instanceof UntitledTextEditorInput || editor instanceof TextResourceEditorInput) { const viewState = this.loadTextEditorViewState(editor.resource); if (viewState) { control.restoreViewState(viewState); @@ -144,7 +146,7 @@ export class AbstractTextResourceEditor extends BaseTextEditor { } private saveTextResourceEditorViewState(input: EditorInput | undefined): void { - if (!(input instanceof UntitledTextEditorInput) && !(input instanceof ResourceEditorInput)) { + if (!(input instanceof UntitledTextEditorInput) && !(input instanceof TextResourceEditorInput)) { return; // only enabled for untitled and resource inputs } @@ -180,7 +182,7 @@ export class TextResourceEditor extends AbstractTextResourceEditor { super(TextResourceEditor.ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService); } - protected override createEditorControl(parent: HTMLElement, configuration: IEditorOptions): IEditor { + protected override createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): IEditor { const control = super.createEditorControl(parent, configuration); // Install a listener for paste to update this editors diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index 3b0e8eafb3..73a182ae91 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -4,16 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/titlecontrol'; +import { localize } from 'vs/nls'; import { applyDragImage, DataTransfers } from 'vs/base/browser/dnd'; import { addDisposableListener, Dimension, EventType } from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { ActionsOrientation, IActionViewItem, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, SubmenuAction } from 'vs/base/common/actions'; +import { IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, SubmenuAction, ActionRunner } 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 { 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'; @@ -26,18 +25,17 @@ 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 { IThemeService, registerThemingParticipant, Themable } from 'vs/platform/theme/common/themeService'; -import { DraggedEditorGroupIdentifier, DraggedEditorIdentifier, fillResourceDataTransfers, LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; +import { DraggedEditorGroupIdentifier, DraggedEditorIdentifier, fillEditorsDragData, LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; import { BreadcrumbsControl, IBreadcrumbsControlOptions } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; import { IEditorGroupsAccessor, IEditorGroupTitleHeight, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; -import { EditorCommandsContextActionRunner, IEditorCommandsContext, IEditorInput, EditorResourceAccessor, IEditorPartOptions, SideBySideEditor, ActiveEditorPinnedContext, ActiveEditorStickyContext } from 'vs/workbench/common/editor'; +import { IEditorCommandsContext, IEditorInput, EditorResourceAccessor, IEditorPartOptions, SideBySideEditor, ActiveEditorPinnedContext, ActiveEditorStickyContext, EditorsOrder } from 'vs/workbench/common/editor'; import { ResourceContextKey } from 'vs/workbench/common/resources'; 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'; import { isFirefox } from 'vs/base/browser/browser'; -import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { isPromiseCanceledError } from 'vs/base/common/errors'; export interface IToolbarActions { @@ -59,6 +57,19 @@ export interface ITitleControlDimensions { available: Dimension; } +export class EditorCommandsContextActionRunner extends ActionRunner { + + constructor( + private context: IEditorCommandsContext + ) { + super(); + } + + override run(action: IAction): Promise { + return super.run(action, this.context); + } +} + export abstract class TitleControl extends Themable { protected readonly groupTransfer = LocalSelectionTransfer.getInstance(); @@ -255,11 +266,16 @@ export abstract class TitleControl extends Themable { e.dataTransfer.effectAllowed = 'copyMove'; } - // If tabs are disabled, treat dragging as if an editor tab was dragged + // Drag all tabs of the group if tabs are enabled let hasDataTransfer = false; - if (!this.accessor.partOptions.showTabs) { + if (this.accessor.partOptions.showTabs) { + hasDataTransfer = this.doFillResourceDataTransfers(this.group.getEditors(EditorsOrder.SEQUENTIAL), e); + } + + // Otherwise only drag the active editor + else { if (this.group.activeEditor) { - hasDataTransfer = this.doFillResourceDataTransfers(this.group.activeEditor, e); + hasDataTransfer = this.doFillResourceDataTransfers([this.group.activeEditor], e); } } @@ -285,29 +301,14 @@ export abstract class TitleControl extends Themable { })); } - protected doFillResourceDataTransfers(editor: IEditorInput, e: DragEvent): boolean { - const resource = EditorResourceAccessor.getOriginalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY }); - if (!resource) { - return false; + protected doFillResourceDataTransfers(editors: readonly IEditorInput[], e: DragEvent): boolean { + if (editors.length) { + this.instantiationService.invokeFunction(fillEditorsDragData, editors.map(editor => ({ editor, groupId: this.group.id })), e); + + return true; } - const editorOptions: ITextEditorOptions = { - viewState: (() => { - if (this.group.activeEditor === editor) { - const activeControl = this.group.activeEditorPane?.getControl(); - if (isCodeEditor(activeControl)) { - return withNullAsUndefined(activeControl.saveViewState()); - } - } - - return undefined; - })(), - sticky: this.group.isSticky(editor) - }; - - this.instantiationService.invokeFunction(fillResourceDataTransfers, [resource], () => editorOptions, e); - - return true; + return false; } protected onContextMenu(editor: IEditorInput, e: Event, node: HTMLElement): void { @@ -381,6 +382,8 @@ export abstract class TitleControl extends Themable { abstract updateEditorLabel(editor: IEditorInput): void; + abstract updateEditorCapabilities(editor: IEditorInput): void; + abstract updateEditorLabels(): void; abstract updateEditorDirty(editor: IEditorInput): void; diff --git a/src/vs/workbench/browser/parts/editor/workspaceTrustRequiredEditor.ts b/src/vs/workbench/browser/parts/editor/workspaceTrustRequiredEditor.ts new file mode 100644 index 0000000000..65161bc0e7 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/workspaceTrustRequiredEditor.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * 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/workspacetrusteditor'; +import { localize } from 'vs/nls'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { Dimension, size, clearNode, append, addDisposableListener, EventType, $ } from 'vs/base/browser/dom'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { EditorDescriptor } from 'vs/workbench/browser/editor'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; + +export class WorkspaceTrustRequiredEditor extends EditorPane { + + static readonly ID = 'workbench.editors.workspaceTrustRequiredEditor'; + static readonly LABEL = localize('trustRequiredEditor', "Workspace Trust Required"); + static readonly DESCRIPTOR = EditorDescriptor.create(WorkspaceTrustRequiredEditor, WorkspaceTrustRequiredEditor.ID, WorkspaceTrustRequiredEditor.LABEL); + + private container: HTMLElement | undefined; + private scrollbar: DomScrollableElement | undefined; + private inputDisposable = this._register(new MutableDisposable()); + + constructor( + @ITelemetryService telemetryService: ITelemetryService, + @IThemeService themeService: IThemeService, + @ICommandService private readonly commandService: ICommandService, + @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, + @IStorageService storageService: IStorageService + ) { + super(WorkspaceTrustRequiredEditor.ID, telemetryService, themeService, storageService); + } + + override getTitle(): string { + return WorkspaceTrustRequiredEditor.LABEL; + } + + protected createEditor(parent: HTMLElement): void { + + // Container + this.container = document.createElement('div'); + this.container.className = 'monaco-workspace-trust-required-editor'; + this.container.style.outline = 'none'; + this.container.tabIndex = 0; // enable focus support from the editor part (do not remove) + + // Custom Scrollbars + this.scrollbar = this._register(new DomScrollableElement(this.container, { horizontal: ScrollbarVisibility.Auto, vertical: ScrollbarVisibility.Auto })); + parent.appendChild(this.scrollbar.getDomNode()); + } + + override async setInput(input: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + await super.setInput(input, options, context, token); + + // Check for cancellation + if (token.isCancellationRequested) { + return; + } + + // Render Input + this.inputDisposable.value = this.renderInput(); + } + + private renderInput(): IDisposable { + const [container, scrollbar] = assertAllDefined(this.container, this.scrollbar); + + clearNode(container); + + const disposables = new DisposableStore(); + + const label = container.appendChild(document.createElement('p')); + label.textContent = isSingleFolderWorkspaceIdentifier(toWorkspaceIdentifier(this.workspaceService.getWorkspace())) ? + localize('requiresFolderTrustText', "The file is not displayed in the editor because trust has not been granted to the folder.") : + localize('requiresWorkspaceTrustText', "The file is not displayed in the editor because trust has not been granted to the workspace."); + + const link = append(label, $('a.embedded-link')); + link.setAttribute('role', 'button'); + link.textContent = localize('manageTrust', "Manage Workspace Trust"); + + disposables.add(addDisposableListener(link, EventType.CLICK, async () => { + await this.commandService.executeCommand('workbench.trust.manage'); + })); + + scrollbar.scanDomNode(); + + return disposables; + } + + override clearInput(): void { + if (this.container) { + clearNode(this.container); + } + + this.inputDisposable.clear(); + + super.clearInput(); + } + + layout(dimension: Dimension): void { + + // Pass on to Container + const [container, scrollbar] = assertAllDefined(this.container, this.scrollbar); + size(container, dimension.width, dimension.height); + scrollbar.scanDomNode(); + } + + override focus(): void { + const container = assertIsDefined(this.container); + + container.focus(); + } + + override dispose(): void { + this.container?.remove(); + + super.dispose(); + } +} diff --git a/src/vs/workbench/browser/parts/media/compositepart.css b/src/vs/workbench/browser/parts/media/compositepart.css index 0b1b054912..b4f48c7276 100644 --- a/src/vs/workbench/browser/parts/media/compositepart.css +++ b/src/vs/workbench/browser/parts/media/compositepart.css @@ -14,4 +14,4 @@ .monaco-workbench .part > .composite.title > .title-actions { flex: 1; padding-left: 5px; -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts index 028cd2e835..a0c62f57ee 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts @@ -122,7 +122,7 @@ export class ConfigureNotificationAction extends Action { constructor( id: string, label: string, - public readonly configurationActions: readonly IAction[] + readonly configurationActions: readonly IAction[] ) { super(id, label, ThemeIcon.asClassName(configureIcon)); } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts index 376ef41c68..052cf4e3a9 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts @@ -71,6 +71,7 @@ export class NotificationsStatus extends Disposable { // Show the bell with a dot if there are unread or in-progress notifications const statusProperties: IStatusbarEntry = { + name: localize('status.notifications', "Notifications"), text: `${notificationsInProgress > 0 || this.newNotificationsCount > 0 ? '$(bell-dot)' : '$(bell)'}`, ariaLabel: localize('status.notifications', "Notifications"), command: this.isNotificationsCenterVisible ? HIDE_NOTIFICATIONS_CENTER : SHOW_NOTIFICATIONS_CENTER, @@ -82,7 +83,6 @@ export class NotificationsStatus extends Disposable { this.notificationsCenterStatusItem = this.statusbarService.addEntry( statusProperties, 'status.notifications', - localize('status.notifications', "Notifications"), StatusbarAlignment.RIGHT, -Number.MAX_VALUE /* towards the far end of the right hand side */ ); @@ -180,9 +180,12 @@ export class NotificationsStatus extends Disposable { let statusMessageEntry: IStatusbarEntryAccessor; let showHandle: any = setTimeout(() => { statusMessageEntry = this.statusbarService.addEntry( - { text: message, ariaLabel: message }, + { + name: localize('status.message', "Status Message"), + text: message, + ariaLabel: message + }, 'status.message', - localize('status.message', "Status Message"), StatusbarAlignment.LEFT, -Number.MAX_VALUE /* far right on left hand side */ ); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts b/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts index 935d39694b..a32ecb81c6 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index c9cf5b138f..f203386b83 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list'; -import { clearNode, addDisposableListener, EventType, EventHelper, $ } from 'vs/base/browser/dom'; +import { clearNode, addDisposableListener, EventType, EventHelper, $, EventLike } from 'vs/base/browser/dom'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; @@ -24,6 +24,9 @@ import { Severity } from 'vs/platform/notification/common/notification'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { Codicon } from 'vs/base/common/codicons'; import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; +import { DomEmitter } from 'vs/base/browser/event'; +import { Gesture, EventType as GestureEventType } from 'vs/base/browser/touch'; +import { Event } from 'vs/base/common/event'; export class NotificationsListDelegate implements IListVirtualDelegate { @@ -151,10 +154,17 @@ class NotificationMessageRenderer { const anchor = $('a', { href: node.href, title: title, }, node.label); if (actionHandler) { - actionHandler.toDispose.add(addDisposableListener(anchor, EventType.CLICK, e => { + const onPointer = (e: EventLike) => { EventHelper.stop(e, true); actionHandler.callback(node.href); - })); + }; + + const onClick = actionHandler.toDispose.add(new DomEmitter(anchor, 'click')).event; + + actionHandler.toDispose.add(Gesture.addTarget(anchor)); + const onTap = actionHandler.toDispose.add(new DomEmitter(anchor, GestureEventType.Tap)).event; + + Event.any(onClick, onTap)(onPointer, null, actionHandler.toDispose); } messageContainer.appendChild(anchor); diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index bd0deff057..5a250e58f6 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -35,6 +35,7 @@ import { CompositeDragAndDropObserver } from 'vs/workbench/browser/dnd'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { CATEGORIES } from 'vs/workbench/common/actions'; +import { Gesture, EventType as GestureEventType } from 'vs/base/browser/touch'; export class SidebarPart extends CompositePart implements IViewletService { @@ -117,9 +118,6 @@ 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(); } @@ -174,6 +172,10 @@ export class SidebarPart extends CompositePart implements IViewletServi this._register(addDisposableListener(titleArea, EventType.CONTEXT_MENU, e => { this.onTitleAreaContextMenu(new StandardMouseEvent(e)); })); + this._register(Gesture.addTarget(titleArea)); + this._register(addDisposableListener(titleArea, GestureEventType.Contextmenu, e => { + this.onTitleAreaContextMenu(new StandardMouseEvent(e)); + })); this.titleLabelElement!.draggable = true; diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 38ebb76563..0d6c6ac53c 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -10,11 +10,12 @@ import { dispose, IDisposable, Disposable, toDisposable, MutableDisposable } fro 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 { EventType as TouchEventType, Gesture, GestureEvent } from 'vs/base/browser/touch'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; 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 { Action, IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, Separator, toAction } from 'vs/base/common/actions'; 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'; @@ -42,13 +43,38 @@ import { syncing } from 'vs/platform/theme/common/iconRegistry'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { hash } from 'vs/base/common/hash'; +import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { isMarkdownString, markdownStringEqual } from 'vs/base/common/htmlContent'; +import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; + +interface IStatusbarEntryPriority { + + /** + * The main priority of the entry that + * defines the order of appearance. + * + * May not be unique across all entries. + */ + primary: number; + + /** + * The secondary priority of the entry + * is used in case the main priority + * matches another one's priority. + * + * Should be unique across all entries. + */ + secondary: number; +} interface IPendingStatusbarEntry { id: string; - name: string; entry: IStatusbarEntry; alignment: StatusbarAlignment; - priority: number; + priority: IStatusbarEntryPriority; accessor?: IStatusbarEntryAccessor; } @@ -56,7 +82,7 @@ interface IStatusbarViewModelEntry { id: string; name: string; alignment: StatusbarAlignment; - priority: number; + priority: IStatusbarEntryPriority; container: HTMLElement; labelContainer: HTMLElement; } @@ -294,14 +320,16 @@ class StatusbarViewModel extends Disposable { this._entries.sort((entryA, entryB) => { if (entryA.alignment === entryB.alignment) { - if (entryA.priority !== entryB.priority) { - return entryB.priority - entryA.priority; // higher priority towards the left + if (entryA.priority.primary !== entryB.priority.primary) { + return entryB.priority.primary - entryA.priority.primary; // higher priority towards the left (primary) } - const indexA = mapEntryToIndex.get(entryA); - const indexB = mapEntryToIndex.get(entryB); + if (entryA.priority.secondary !== entryB.priority.secondary) { + return entryB.priority.secondary - entryA.priority.secondary; // higher priority towards the left (secondary) + } - return indexA! - indexB!; // otherwise maintain stable order (both values known to be in map) + // otherwise maintain stable order (both values known to be in map) + return mapEntryToIndex.get(entryA)! - mapEntryToIndex.get(entryB)!; } if (entryA.alignment === StatusbarAlignment.LEFT) { @@ -404,6 +432,8 @@ export class StatusbarPart extends Part implements IStatusbarService { private leftItemsContainer: HTMLElement | undefined; private rightItemsContainer: HTMLElement | undefined; + private hoverDelegate: IHoverDelegate; + constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @@ -412,30 +442,42 @@ export class StatusbarPart extends Part implements IStatusbarService { @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @IContextMenuService private contextMenuService: IContextMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IHoverService hoverService: IHoverService, + @IConfigurationService configurationService: IConfigurationService ) { super(Parts.STATUSBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); this.registerListeners(); + + this.hoverDelegate = { + showHover: (options: IHoverDelegateOptions) => hoverService.showHover(options), + delay: configurationService.getValue('workbench.hover.delay'), + placement: 'element' + }; } private registerListeners(): void { this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateStyles())); } - addEntry(entry: IStatusbarEntry, id: string, name: string, alignment: StatusbarAlignment, priority: number = 0): IStatusbarEntryAccessor { + addEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, primaryPriority = 0): IStatusbarEntryAccessor { + const priority: IStatusbarEntryPriority = { + primary: primaryPriority, + secondary: hash(id) // derive from identifier to accomplish uniqueness + }; // As long as we have not been created into a container yet, record all entries // that are pending so that they can get created at a later point if (!this.element) { - return this.doAddPendingEntry(entry, id, name, alignment, priority); + return this.doAddPendingEntry(entry, id, alignment, priority); } // Otherwise add to view - return this.doAddEntry(entry, id, name, alignment, priority); + return this.doAddEntry(entry, id, alignment, priority); } - private doAddPendingEntry(entry: IStatusbarEntry, id: string, name: string, alignment: StatusbarAlignment, priority: number): IStatusbarEntryAccessor { - const pendingEntry: IPendingStatusbarEntry = { entry, id, name, alignment, priority }; + private doAddPendingEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priority: IStatusbarEntryPriority): IStatusbarEntryAccessor { + const pendingEntry: IPendingStatusbarEntry = { entry, id, alignment, priority }; this.pendingEntries.push(pendingEntry); const accessor: IStatusbarEntryAccessor = { @@ -459,17 +501,25 @@ export class StatusbarPart extends Part implements IStatusbarService { return accessor; } - private doAddEntry(entry: IStatusbarEntry, id: string, name: string, alignment: StatusbarAlignment, priority: number): IStatusbarEntryAccessor { + private doAddEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priority: IStatusbarEntryPriority): IStatusbarEntryAccessor { // Create item const itemContainer = this.doCreateStatusItem(id, alignment, ...coalesce([entry.showBeak ? 'has-beak' : undefined])); - const item = this.instantiationService.createInstance(StatusbarEntryItem, itemContainer, entry); + const item = this.instantiationService.createInstance(StatusbarEntryItem, itemContainer, entry, this.hoverDelegate); // Append to parent this.appendOneStatusbarEntry(itemContainer, alignment, priority); // Add to view model - const viewModelEntry: IStatusbarViewModelEntry = { id, name, alignment, priority, container: itemContainer, labelContainer: item.labelContainer }; + const viewModelEntry: IStatusbarViewModelEntry = new class implements IStatusbarViewModelEntry { + readonly id = id; + readonly alignment = alignment; + readonly priority = priority; + readonly container = itemContainer; + readonly labelContainer = item.labelContainer; + + get name() { return item.name; } + }; const viewModelEntryDispose = this.viewModel.add(viewModelEntry); return { @@ -537,6 +587,8 @@ export class StatusbarPart extends Part implements IStatusbarService { // Context menu support this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, e => this.showContextMenu(e))); + this._register(Gesture.addTarget(parent)); + this._register(addDisposableListener(parent, TouchEventType.Contextmenu, e => this.showContextMenu(e))); // Initial status bar entries this.createInitialStatusbarEntries(); @@ -553,7 +605,7 @@ export class StatusbarPart extends Part implements IStatusbarService { while (this.pendingEntries.length) { const pending = this.pendingEntries.shift(); if (pending) { - pending.accessor = this.addEntry(pending.entry, pending.id, pending.name, pending.alignment, pending.priority); + pending.accessor = this.addEntry(pending.entry, pending.id, pending.alignment, pending.priority.primary); } } } @@ -571,7 +623,7 @@ export class StatusbarPart extends Part implements IStatusbarService { }); } - private appendOneStatusbarEntry(itemContainer: HTMLElement, alignment: StatusbarAlignment, priority: number): void { + private appendOneStatusbarEntry(itemContainer: HTMLElement, alignment: StatusbarAlignment, priority: IStatusbarEntryPriority): void { const entries = this.viewModel.getEntries(alignment); if (alignment === StatusbarAlignment.RIGHT) { @@ -584,9 +636,20 @@ export class StatusbarPart extends Part implements IStatusbarService { // and then insert the item before that one let appended = false; for (const entry of entries) { + + // pick a priority that ideally is not the same + // by falling back to secondary priority + let existingEntryPriority = entry.priority.primary; + let newEntryPriority = priority.primary; + if (existingEntryPriority === newEntryPriority) { + existingEntryPriority = entry.priority.secondary; + newEntryPriority = priority.secondary; + } + + // insert according to priority if ( - alignment === StatusbarAlignment.LEFT && entry.priority < priority || - alignment === StatusbarAlignment.RIGHT && entry.priority > priority // reversing due to flex: row-reverse + alignment === StatusbarAlignment.LEFT && existingEntryPriority < newEntryPriority || + alignment === StatusbarAlignment.RIGHT && existingEntryPriority > newEntryPriority // reversing due to flex: row-reverse ) { target.insertBefore(itemContainer, entry.container); appended = true; @@ -600,7 +663,7 @@ export class StatusbarPart extends Part implements IStatusbarService { } } - private showContextMenu(e: MouseEvent): void { + private showContextMenu(e: MouseEvent | GestureEvent): void { EventHelper.stop(e, true); const event = new StandardMouseEvent(e); @@ -625,7 +688,7 @@ export class StatusbarPart extends Part implements IStatusbarService { const actions: IAction[] = []; // Provide an action to hide the status bar at last - actions.push(this.instantiationService.createInstance(ToggleStatusbarVisibilityAction, ToggleStatusbarVisibilityAction.ID, localize('hideStatusBar', "Hide Status Bar"))); + actions.push(toAction({ id: ToggleStatusbarVisibilityAction.ID, label: localize('hideStatusBar', "Hide Status Bar"), run: () => this.instantiationService.invokeFunction(accessor => new ToggleStatusbarVisibilityAction().run(accessor)) })); actions.push(new Separator()); // Show an entry per known status entry @@ -718,7 +781,7 @@ export class StatusbarPart extends Part implements IStatusbarService { class StatusBarCodiconLabel extends SimpleIconLabel { - private readonly progressCodicon: any = renderIcon(syncing); + private readonly progressCodicon = renderIcon(syncing); private currentText = ''; private currentShowProgress = false; @@ -776,7 +839,10 @@ class StatusbarEntryItem extends Disposable { readonly labelContainer: HTMLElement; private readonly label: StatusBarCodiconLabel; + private customHover: IDisposable | undefined; + private entry: IStatusbarEntry | undefined = undefined; + get name(): string { return assertIsDefined(this.entry).name; } private readonly foregroundListener = this._register(new MutableDisposable()); private readonly backgroundListener = this._register(new MutableDisposable()); @@ -787,6 +853,7 @@ class StatusbarEntryItem extends Disposable { constructor( private container: HTMLElement, entry: IStatusbarEntry, + private readonly customHoverDelegate: IHoverDelegate, @ICommandService private readonly commandService: ICommandService, @INotificationService private readonly notificationService: INotificationService, @ITelemetryService private readonly telemetryService: ITelemetryService, @@ -835,8 +902,15 @@ class StatusbarEntryItem extends Disposable { } // Update: Tooltip (on the container, because label can be disabled) - if (!this.entry || entry.tooltip !== this.entry.tooltip) { - if (entry.tooltip) { + if (!this.entry || !isEqualTooltip(this.entry, entry)) { + if (this.customHover) { + this.customHover.dispose(); + this.customHover = undefined; + } + if (isMarkdownString(entry.tooltip)) { + this.container.removeAttribute('title'); + this.customHover = setupCustomHover(this.customHoverDelegate, this.container, { markdown: entry.tooltip, markdownNotSupportedFallback: undefined }); + } else if (entry.tooltip) { this.container.title = entry.tooltip; } else { this.container.title = ''; @@ -947,9 +1021,24 @@ class StatusbarEntryItem extends Disposable { dispose(this.backgroundListener); dispose(this.commandMouseListener); dispose(this.commandKeyboardListener); + if (this.customHover) { + this.customHover.dispose(); + } } } +function isEqualTooltip(e1: IStatusbarEntry, e2: IStatusbarEntry) { + const t1 = e1.tooltip; + const t2 = e2.tooltip; + if (t1 === undefined) { + return t2 === undefined; + } + if (isMarkdownString(t1)) { + return isMarkdownString(t2) && markdownStringEqual(t1, t2); + } + return t1 === t2; +} + registerThemingParticipant((theme, collector) => { if (theme.type !== ColorScheme.HIGH_CONTRAST) { const statusBarItemHoverBackground = theme.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND); diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index 3f78a9171f..944760199e 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -4,17 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { IMenuService, MenuId, IMenu, SubmenuItemAction, registerAction2, Action2, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId, IMenu, SubmenuItemAction, registerAction2, Action2, MenuItemAction, MenuRegistry } 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 { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; // {{SQL CARBON EDIT}} import { IAction, Action, SubmenuAction, Separator } from 'vs/base/common/actions'; import { addDisposableListener, Dimension, EventType } from 'vs/base/browser/dom'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; 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'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IRecentlyOpened, isRecentFolder, IRecent, isRecentWorkspace, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { RunOnceScheduler } from 'vs/base/common/async'; import { MENUBAR_SELECTION_FOREGROUND, MENUBAR_SELECTION_BACKGROUND, MENUBAR_SELECTION_BORDER, TITLE_BAR_ACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_FOREGROUND, ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_INACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; @@ -36,11 +36,97 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; 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 { IsMacNativeContext, IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; import { ICommandService } from 'vs/platform/commands/common/commands'; export type IOpenRecentAction = IAction & { uri: URI, remoteAuthority?: string }; +MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, { + submenu: MenuId.MenubarFileMenu, + title: { + value: 'File', + original: 'File', + mnemonicTitle: localize({ key: 'mFile', comment: ['&& denotes a mnemonic'] }, "&&File"), + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, { + submenu: MenuId.MenubarEditMenu, + title: { + value: 'Edit', + original: 'Edit', + mnemonicTitle: localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit") + }, + order: 2 +}); + +/* {{SQL CARBON EDIT}} - Disable unused menus +MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, { + submenu: MenuId.MenubarSelectionMenu, + title: { + value: 'Selection', + original: 'Selection', + mnemonicTitle: localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection") + }, + order: 3 +}); +*/ + +MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, { + submenu: MenuId.MenubarViewMenu, + title: { + value: 'View', + original: 'View', + mnemonicTitle: localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View") + }, + order: 4 +}); + +/* {{SQL CARBON EDIT}} - Disable unused menus +MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, { + submenu: MenuId.MenubarGoMenu, + title: { + value: 'Go', + original: 'Go', + mnemonicTitle: localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Go") + }, + order: 5 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, { + submenu: MenuId.MenubarTerminalMenu, + title: { + value: 'Terminal', + original: 'Terminal', + mnemonicTitle: localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal") + }, + order: 7, + when: ContextKeyExpr.has('terminalProcessSupported') +}); +*/ + +MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, { + submenu: MenuId.MenubarHelpMenu, + title: { + value: 'Help', + original: 'Help', + mnemonicTitle: localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help") + }, + order: 8 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, { + submenu: MenuId.MenubarPreferencesMenu, + title: { + value: 'Preferences', + original: 'Preferences', + mnemonicTitle: localize('mPreferences', "Preferences") + }, + when: IsMacNativeContext, + order: 9 +}); + export abstract class MenubarControl extends Disposable { protected keys = [ @@ -52,28 +138,10 @@ export abstract class MenubarControl extends Disposable { ]; protected menus: { - 'File': IMenu; - 'Edit': IMenu; - // 'Selection': IMenu; {{SQL CARBON EDIT}} - Disable unusued menus - 'View': IMenu; - // 'Go': IMenu; {{SQL CARBON EDIT}} - Disable unusued menus - // 'Run': IMenu; {{SQL CARBON EDIT}} - Disable unusued menus - // 'Terminal': IMenu; {{SQL CARBON EDIT}} - Disable unusued menus - 'Window'?: IMenu; - 'Help': IMenu; [index: string]: IMenu | undefined; - }; + } = {}; - protected topLevelTitles: { [menu: string]: string } = { - 'File': localize({ key: 'mFile', comment: ['&& denotes a mnemonic'] }, "&&File"), - 'Edit': localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit"), - // 'Selection': localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection"), {{SQL CARBON EDIT}} - Disable unused menus - 'View': localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View"), - // 'Go': localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Go"), {{SQL CARBON EDIT}} - Disable unused menus - // 'Run': localize({ key: 'mRun', comment: ['&& denotes a mnemonic'] }, "&&Run"), {{SQL CARBON EDIT}} - Disable unused menus - // 'Terminal': localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal"), {{SQL CARBON EDIT}} - Disable unused menus - 'Help': localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help") - }; + protected topLevelTitles: { [menu: string]: string } = {}; protected recentlyOpened: IRecentlyOpened = { files: [], workspaces: [] }; @@ -100,18 +168,30 @@ export abstract class MenubarControl extends Disposable { super(); - // {{SQL CARBON EDIT}} - Disable unusued menus - this.menus = { - 'File': this._register(this.menuService.createMenu(MenuId.MenubarFileMenu, this.contextKeyService)), - 'Edit': this._register(this.menuService.createMenu(MenuId.MenubarEditMenu, this.contextKeyService)), - // 'Selection': this._register(this.menuService.createMenu(MenuId.MenubarSelectionMenu, this.contextKeyService)), - 'View': this._register(this.menuService.createMenu(MenuId.MenubarViewMenu, this.contextKeyService)), - // 'Go': this._register(this.menuService.createMenu(MenuId.MenubarGoMenu, this.contextKeyService)), - // 'Run': this._register(this.menuService.createMenu(MenuId.MenubarDebugMenu, this.contextKeyService)), - // 'Terminal': this._register(this.menuService.createMenu(MenuId.MenubarTerminalMenu, this.contextKeyService)), - 'Help': this._register(this.menuService.createMenu(MenuId.MenubarHelpMenu, this.contextKeyService)) + const mainMenu = this._register(this.menuService.createMenu(MenuId.MenubarMainMenu, this.contextKeyService)); + const mainMenuDisposables = this._register(new DisposableStore()); + + const setupMenu = () => { + mainMenuDisposables.clear(); + this.menus = {}; + this.topLevelTitles = {}; + + const [, mainMenuActions] = mainMenu.getActions()[0]; + for (const mainMenuAction of mainMenuActions) { + if (mainMenuAction instanceof SubmenuItemAction && typeof mainMenuAction.item.title !== 'string') { + this.menus[mainMenuAction.item.title.original] = mainMenuDisposables.add(this.menuService.createMenu(mainMenuAction.item.submenu, this.contextKeyService)); + this.topLevelTitles[mainMenuAction.item.title.original] = mainMenuAction.item.title.mnemonicTitle ?? mainMenuAction.item.title.value; + } + } }; + setupMenu(); + + mainMenu.onDidChange(() => { + setupMenu(); + this.doUpdateMenubar(true); + }); + this.menuUpdater = this._register(new RunOnceScheduler(() => this.doUpdateMenubar(false), 200)); this.notifyUserOfCustomMenubarAccessibility(); @@ -552,6 +632,7 @@ export class CustomMenubarControl extends MenubarControl { this._onVisibilityChange.fire(visible); } + private reinstallDisposables = this._register(new DisposableStore()); private setupCustomMenubar(firstTime: boolean): void { // If there is no container, we cannot setup the menubar if (!this.container) { @@ -559,14 +640,19 @@ export class CustomMenubarControl extends MenubarControl { } if (firstTime) { - this.menubar = this._register(new MenuBar(this.container, this.getMenuBarOptions())); + // Reset and create new menubar + if (this.menubar) { + this.reinstallDisposables.clear(); + } + + this.menubar = this.reinstallDisposables.add(new MenuBar(this.container, this.getMenuBarOptions())); this.accessibilityService.alwaysUnderlineAccessKeys().then(val => { this.alwaysOnMnemonics = val; this.menubar?.update(this.getMenuBarOptions()); }); - this._register(this.menubar.onFocusStateChange(focused => { + this.reinstallDisposables.add(this.menubar.onFocusStateChange(focused => { this._onFocusStateChange.fire(focused); // When the menubar loses focus, update it to clear any pending updates @@ -576,18 +662,18 @@ export class CustomMenubarControl extends MenubarControl { } })); - this._register(this.menubar.onVisibilityChange(e => this.onDidVisibilityChange(e))); + this.reinstallDisposables.add(this.menubar.onVisibilityChange(e => this.onDidVisibilityChange(e))); // Before we focus the menubar, stop updates to it so that focus-related context keys will work - this._register(addDisposableListener(this.container, EventType.FOCUS_IN, () => { + this.reinstallDisposables.add(addDisposableListener(this.container, EventType.FOCUS_IN, () => { this.focusInsideMenubar = true; })); - this._register(addDisposableListener(this.container, EventType.FOCUS_OUT, () => { + this.reinstallDisposables.add(addDisposableListener(this.container, EventType.FOCUS_OUT, () => { this.focusInsideMenubar = false; })); - this._register(attachMenuStyler(this.menubar, this.themeService)); + this.reinstallDisposables.add(attachMenuStyler(this.menubar, this.themeService)); } else { this.menubar?.update(this.getMenuBarOptions()); } @@ -655,7 +741,7 @@ export class CustomMenubarControl extends MenubarControl { for (const title of Object.keys(this.topLevelTitles)) { const menu = this.menus[title]; if (firstTime && menu) { - this._register(menu.onDidChange(() => { + this.reinstallDisposables.add(menu.onDidChange(() => { if (!this.focusInsideMenubar) { const actions: IAction[] = []; updateActions(menu, actions, title); @@ -667,7 +753,7 @@ export class CustomMenubarControl extends MenubarControl { // For the file menu, we need to update if the web nav menu updates as well if (menu === this.menus.File) { - this._register(this.webNavigationMenu.onDidChange(() => { + this.reinstallDisposables.add(this.webNavigationMenu.onDidChange(() => { if (!this.focusInsideMenubar) { const actions: IAction[] = []; updateActions(menu, actions, title); diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 4cc4d1e554..2a02210170 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -166,6 +166,7 @@ export class TitlebarPart extends Part implements ITitleService { if (activeEditor) { this.activeEditorListeners.add(activeEditor.onDidChangeDirty(() => this.titleUpdater.schedule())); this.activeEditorListeners.add(activeEditor.onDidChangeLabel(() => this.titleUpdater.schedule())); + this.activeEditorListeners.add(activeEditor.onDidChangeCapabilities(() => this.titleUpdater.schedule())); } } diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index 5b6e1c318f..a72b029207 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -143,12 +143,17 @@ padding-right: 6px; width: 16px; height: 22px; + display: flex; + align-items: center; + justify-content: center; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item > .custom-view-tree-node-item-icon.codicon { - margin-top: 3px; +/* makes spinning icons square */ +.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item > .custom-view-tree-node-item-icon.codicon.codicon-modifier-spin { + padding-left: 6px; + margin-left: -6px; } .customview-tree .monaco-list .monaco-list-row.selected .custom-view-tree-node-item > .custom-view-tree-node-item-icon.codicon { diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts index ceef0c3881..51a90004ff 100644 --- a/src/vs/workbench/browser/parts/views/viewPane.ts +++ b/src/vs/workbench/browser/parts/views/viewPane.ts @@ -7,7 +7,7 @@ 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 { attachButtonStyler, 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'; @@ -40,7 +40,7 @@ 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'; +import { CompositeMenuActions } from 'vs/workbench/browser/actions'; export interface IViewPaneOptions extends IPaneOptions { id: string; @@ -445,6 +445,10 @@ export abstract class ViewPane extends Pane implements IView { this.scrollableElement.scanDomNode(); } + onDidScrollRoot() { + // noop + } + getProgressIndicator() { if (this.progressBar === undefined) { // Progress bar @@ -575,13 +579,12 @@ export abstract class ViewPane extends Pane implements IView { if (typeof node === 'string') { append(p, document.createTextNode(node)); } else { - const link = this.instantiationService.createInstance(Link, node); + 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) }); + const updateEnablement = () => link.enabled = this.contextKeyService.contextMatchesRules(precondition); updateEnablement(); const keys = new Set(); diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 34bf9a923a..59bbf77626 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -35,9 +35,10 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; -import { CompositeMenuActions } from 'vs/workbench/browser/menuActions'; +import { CompositeMenuActions } from 'vs/workbench/browser/actions'; import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Gesture, EventType as TouchEventType } from 'vs/base/browser/touch'; export const ViewsSubMenu = new MenuId('Views'); MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { @@ -407,7 +408,10 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { options.orientation = this.orientation; this.paneview = this._register(new PaneView(parent, this.options)); this._register(this.paneview.onDidDrop(({ from, to }) => this.movePane(from as ViewPane, to as ViewPane))); + this._register(this.paneview.onDidScroll(_ => this.onDidScrollPane())); this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, (e: MouseEvent) => this.showContextMenu(new StandardMouseEvent(e)))); + this._register(Gesture.addTarget(parent)); + this._register(addDisposableListener(parent, TouchEventType.Contextmenu, (e: MouseEvent) => this.showContextMenu(new StandardMouseEvent(e)))); this._menuActions = this._register(this.instantiationService.createInstance(ViewContainerMenuActions, this.paneview.element, this.viewContainer)); this._register(this._menuActions.onDidChange(() => this.updateTitleArea())); @@ -1064,6 +1068,12 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { return true; } + private onDidScrollPane() { + for (const pane of this.panes) { + pane.onDidScrollRoot(); + } + } + override dispose(): void { super.dispose(); this.paneItems.forEach(i => i.disposable.dispose()); diff --git a/src/vs/workbench/browser/style.ts b/src/vs/workbench/browser/style.ts index 22a2105a16..9f42a1367c 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 } from 'vs/platform/theme/common/themeService'; -import { iconForeground, foreground, selectionBackground, focusBorder, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, listHighlightForeground, inputPlaceholderForeground, toolbarHoverBackground, toolbarActiveBackground, toolbarHoverOutline } from 'vs/platform/theme/common/colorRegistry'; +import { iconForeground, foreground, selectionBackground, focusBorder, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, listHighlightForeground, inputPlaceholderForeground, toolbarHoverBackground, toolbarActiveBackground, toolbarHoverOutline, listFocusHighlightForeground } 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'; import { createMetaElement } from 'vs/base/browser/dom'; @@ -61,6 +61,16 @@ registerThemingParticipant((theme, collector) => { `); } + // List highlight w/ focus + const listHighlightFocusForegroundColor = theme.getColor(listFocusHighlightForeground); + if (listHighlightFocusForegroundColor) { + collector.addRule(` + .monaco-workbench .monaco-list .monaco-list-row.focused .monaco-highlighted-label .highlight { + color: ${listHighlightFocusForegroundColor}; + } + `); + } + // Scrollbars const scrollbarShadowColor = theme.getColor(scrollbarShadow); if (scrollbarShadowColor) { diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index b16c323961..b25a20c22a 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -63,6 +63,7 @@ import { ITimerService } from 'vs/workbench/services/timer/browser/timerService' import { WorkspaceTrustManagementService } from 'vs/workbench/services/workspaces/common/workspaceTrust'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { HTMLFileSystemProvider } from 'vs/platform/files/browser/htmlFileSystemProvider'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; class BrowserMain extends Disposable { @@ -106,16 +107,22 @@ class BrowserMain extends Disposable { const commandService = accessor.get(ICommandService); const lifecycleService = accessor.get(ILifecycleService); const timerService = accessor.get(ITimerService); + const openerService = accessor.get(IOpenerService); + const productService = accessor.get(IProductService); return { commands: { executeCommand: (command, ...args) => commandService.executeCommand(command, ...args) }, env: { + uriScheme: productService.urlProtocol, async retrievePerformanceMarks() { await timerService.whenReady(); return timerService.getPerformanceMarks(); + }, + async openUri(uri: URI): Promise { + return openerService.open(uri, {}); } }, shutdown: () => lifecycleService.shutdown() @@ -176,7 +183,7 @@ class BrowserMain extends Disposable { serviceCollection.set(IFileService, fileService); await this.registerFileSystemProviders(environmentService, fileService, remoteAgentService, logService, logsPath); - // IURIIdentityService + // URI Identity const uriIdentityService = new UriIdentityService(fileService); serviceCollection.set(IUriIdentityService, uriIdentityService); @@ -203,7 +210,7 @@ class BrowserMain extends Disposable { ]); // Workspace Trust Service - const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, environmentService, storageService, uriIdentityService, configurationService); + const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, storageService, uriIdentityService, environmentService, configurationService, remoteAuthorityResolverService); serviceCollection.set(IWorkspaceTrustManagementService, workspaceTrustManagementService); // Update workspace trust so that configuration is updated accordingly @@ -306,12 +313,16 @@ class BrowserMain extends Disposable { async run(accessor: ServicesAccessor): Promise { const dialogService = accessor.get(IDialogService); const hostService = accessor.get(IHostService); + const storageService = accessor.get(IStorageService); 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(); + if (storageService instanceof BrowserStorageService) { + await storageService.clear(); + } } hostService.reload(); @@ -324,7 +335,7 @@ class BrowserMain extends Disposable { } private async createStorageService(payload: IWorkspaceInitializationPayload, environmentService: IWorkbenchEnvironmentService, fileService: IFileService, logService: ILogService): Promise { - const storageService = new BrowserStorageService(payload, environmentService, fileService); + const storageService = new BrowserStorageService(payload, logService, environmentService, fileService); try { await storageService.initialize(); diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index 9f947512cd..f488430f56 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ 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 { addDisposableListener, addDisposableThrottledListener, detectFullscreen, EventHelper, EventType, windowOpenNoOpenerWithSuccess, windowOpenNoOpener } from 'vs/base/browser/dom'; +import { DomEmitter } 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'; @@ -50,7 +50,13 @@ export class BrowserWindow extends Disposable { // Layout const viewport = isIOS && window.visualViewport ? window.visualViewport /** Visual viewport */ : window /** Layout viewport */; - this._register(addDisposableListener(viewport, EventType.RESIZE, () => this.onWindowResize())); + this._register(addDisposableListener(viewport, EventType.RESIZE, () => { + this.onWindowResize(); + if (isIOS) { + // Sometimes the keyboard appearing scrolls the whole workbench out of view, as a workaround scroll back into view #121206 + window.scrollTo(0, 0); + } + })); // Prevent the back/forward gestures in macOS this._register(addDisposableListener(this.layoutService.container, EventType.WHEEL, e => e.preventDefault(), { passive: false })); @@ -74,7 +80,6 @@ export class BrowserWindow extends Disposable { private onWindowResize(): void { this.logService.trace(`web.main#${isIOS && window.visualViewport ? 'visualViewport' : 'window'}Resize`); - this.layoutService.layout(); } @@ -84,8 +89,8 @@ export class BrowserWindow extends Disposable { // 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)) + Event.once(new DomEmitter(document.body, EventType.KEY_DOWN, true).event), + Event.once(new DomEmitter(document.body, EventType.MOUSE_DOWN, true).event) )).then(async () => { // Delay the dialog in case the user interacted @@ -139,13 +144,26 @@ export class BrowserWindow extends Disposable { this.openerService.setDefaultExternalOpener({ openExternal: async (href: string) => { if (matchesScheme(href, Schemas.http) || matchesScheme(href, Schemas.https)) { - const opened = windowOpenNoOpener(href); + const opened = windowOpenNoOpenerWithSuccess(href); if (!opened) { - const showResult = await this.dialogService.show(Severity.Warning, localize('unableToOpenExternal', "The browser interrupted the opening of a new tab or window. Press 'Open' to open it anyway."), - [localize('open', "Open"), localize('learnMore', "Learn More"), localize('cancel', "Cancel")], { cancelId: 2, detail: href }); + const showResult = await this.dialogService.show( + Severity.Warning, + localize('unableToOpenExternal', "The browser interrupted the opening of a new tab or window. Press 'Open' to open it anyway."), + [ + localize('open', "Open"), + localize('learnMore', "Learn More"), + localize('cancel', "Cancel") + ], + { + cancelId: 2, + detail: href + } + ); + if (showResult.choice === 0) { windowOpenNoOpener(href); } + if (showResult.choice === 1) { await this.openerService.open(URI.parse('https://aka.ms/allow-vscode-popup')); } @@ -160,13 +178,13 @@ export class BrowserWindow extends Disposable { } private registerLabelFormatters() { - this.labelService.registerFormatter({ + this._register(this.labelService.registerFormatter({ scheme: Schemas.userData, priority: true, formatting: { label: '(Settings) ${path}', separator: '/', } - }); + })); } } diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index 2352a31c68..749f9d3869 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -278,9 +278,7 @@ export class Workbench extends Layout { } private restoreFontInfo(storageService: IStorageService, configurationService: IConfigurationService): void { - - // Restore (native: use storage service, web: use browser specific local storage) - const storedFontInfoRaw = isNative ? storageService.get('editorFontInfo', StorageScope.GLOBAL) : window.localStorage.getItem('vscode.editorFontInfo'); + const storedFontInfoRaw = storageService.get('editorFontInfo', StorageScope.GLOBAL); if (storedFontInfoRaw) { try { const storedFontInfo = JSON.parse(storedFontInfoRaw); @@ -298,17 +296,7 @@ export class Workbench extends Layout { private storeFontInfo(storageService: IStorageService): void { const serializedFontInfo = serializeFontInfo(); if (serializedFontInfo) { - const serializedFontInfoRaw = JSON.stringify(serializedFontInfo); - - // Font info is very specific to the machine the workbench runs - // on. As such, in the web, we prefer to store this info in - // local storage and not global storage because it would not make - // much sense to synchronize to other machines. - if (isNative) { - storageService.store('editorFontInfo', serializedFontInfoRaw, StorageScope.GLOBAL, StorageTarget.MACHINE); - } else { - window.localStorage.setItem('vscode.editorFontInfo', serializedFontInfoRaw); - } + storageService.store('editorFontInfo', JSON.stringify(serializedFontInfo), StorageScope.GLOBAL, StorageTarget.MACHINE); } } @@ -343,6 +331,7 @@ export class Workbench extends Layout { // Create Parts [ { id: Parts.TITLEBAR_PART, role: 'contentinfo', classes: ['titlebar'] }, + { id: Parts.BANNER_PART, role: 'banner', classes: ['banner'] }, { id: Parts.ACTIVITYBAR_PART, role: 'none', classes: ['activitybar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] }, // Use role 'none' for some parts to make screen readers less chatty #114892 { 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 } }, diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 0d2ac054cb..9f424519ce 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -4,24 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { Event, Emitter } from 'vs/base/common/event'; -import { withNullAsUndefined, assertIsDefined } from 'vs/base/common/types'; +import { Event } from 'vs/base/common/event'; +import { assertIsDefined, isUndefinedOrNull } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IEditor, IEditorViewState, ScrollType, IDiffEditor } from 'vs/editor/common/editorCommon'; -import { IEditorModel, IEditorOptions, ITextEditorOptions, IBaseResourceEditorInput, IResourceEditorInput, EditorActivation, EditorOpenContext, ITextEditorSelection, TextEditorSelectionRevealType, EditorOverride } from 'vs/platform/editor/common/editor'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IEditor, IEditorViewState, IDiffEditor } from 'vs/editor/common/editorCommon'; +import { IEditorModel, IEditorOptions, ITextEditorOptions, IBaseResourceEditorInput, IResourceEditorInput, ITextResourceEditorInput, IBaseTextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IInstantiationService, IConstructorSignature0, ServicesAccessor, BrandedService } from 'vs/platform/instantiation/common/instantiation'; import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; import { IEncodingSupport, IModeSupport } from 'vs/workbench/services/textfile/common/textfiles'; import { GroupsOrder, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ICompositeControl, IComposite } from 'vs/workbench/common/composite'; -import { ActionRunner, IAction } from 'vs/base/common/actions'; import { IFileService } from 'vs/platform/files/common/files'; import { IPathData } from 'vs/platform/windows/common/windows'; -import { coalesce, firstOrDefault } from 'vs/base/common/arrays'; +import { coalesce } from 'vs/base/common/arrays'; import { ACTIVE_GROUP, IResourceEditorInputType, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { IRange } from 'vs/editor/common/core/range'; import { IExtUri } from 'vs/base/common/resources'; // Static values for editor contributions @@ -68,6 +66,31 @@ export const TEXT_DIFF_EDITOR_ID = 'workbench.editors.textDiffEditor'; */ export const BINARY_DIFF_EDITOR_ID = 'workbench.editors.binaryResourceDiffEditor'; +export interface IEditorDescriptor { + + /** + * The unique type identifier of the editor. All instances + * of the same `IEditorPane` should have the same type + * identifier. + */ + readonly typeId: string; + + /** + * The display name of the editor. + */ + readonly name: string; + + /** + * Instantiates the editor pane using the provided services. + */ + instantiate(instantiationService: IInstantiationService): T; + + /** + * Whether the descriptor is for the provided editor pane. + */ + describes(editorPane: T): boolean; +} + /** * The editor pane is the container for workbench editors. */ @@ -190,7 +213,7 @@ export interface IFileEditorInputFactory { /** * Creates new new editor input capable of showing files. */ - createFileEditorInput(resource: URI, preferredResource: URI | undefined, preferredName: string | undefined, preferredDescription: string | undefined, preferredEncoding: string | undefined, preferredMode: string | undefined, instantiationService: IInstantiationService): IFileEditorInput; + createFileEditorInput(resource: URI, preferredResource: URI | undefined, preferredName: string | undefined, preferredDescription: string | undefined, preferredEncoding: string | undefined, preferredMode: string | undefined, preferredContents: string | undefined, instantiationService: IInstantiationService): IFileEditorInput; /** * Check if the provided object is a file editor input. @@ -198,22 +221,6 @@ export interface IFileEditorInputFactory { isFileEditorInput(obj: unknown): obj is IFileEditorInput; } -/** - * @deprecated obsolete - * - * TODO@bpasero remove this API and users once the generic backup restorer has been removed - */ -export interface ICustomEditorInputFactory { - /** - * @deprecated obsolete - */ - createCustomEditorInput(resource: URI, instantiationService: IInstantiationService): Promise; - /** - * @deprecated obsolete - */ - canResolveBackup(editorInput: IEditorInput, backupResource: URI): boolean; -} - export interface IEditorInputFactoryRegistry { /** @@ -226,20 +233,6 @@ export interface IEditorInputFactoryRegistry { */ getFileEditorInputFactory(): IFileEditorInputFactory; - /** - * Registers the custom editor input factory to use for custom inputs. - * - * @deprecated obsolete - */ - registerCustomEditorInputFactory(scheme: string, factory: ICustomEditorInputFactory): void; - - /** - * Returns the custom editor input factory to use for custom inputs. - * - * @deprecated obsolete - */ - getCustomEditorInputFactory(scheme: string): ICustomEditorInputFactory | undefined; - /** * Registers a editor input serializer for the given editor input to the registry. * An editor input serializer is capable of serializing and deserializing editor @@ -279,10 +272,10 @@ export interface IEditorInputSerializer { * Returns an editor input from the provided serialized form of the editor input. This form matches * the value returned from the serialize() method. */ - deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput | undefined; + deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): IEditorInput | undefined; } -export interface IUntitledTextResourceEditorInput extends IBaseResourceEditorInput { +export interface IUntitledTextResourceEditorInput extends IBaseTextResourceEditorInput { /** * Optional resource. If the resource is not provided a new untitled file is created (e.g. Untitled-1). @@ -291,34 +284,19 @@ export interface IUntitledTextResourceEditorInput extends IBaseResourceEditorInp * the untitled editor. */ readonly resource?: URI; - - /** - * Optional language of the untitled resource. - */ - readonly mode?: string; - - /** - * Optional contents of the untitled resource. - */ - readonly contents?: string; - - /** - * Optional encoding of the untitled resource. - */ - readonly encoding?: string; } export interface IResourceDiffEditorInput extends IBaseResourceEditorInput { /** - * The left hand side URI to open inside a diff editor. + * The left hand side editor to open inside a diff editor. */ - readonly leftResource: URI; + readonly originalInput: IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput; /** - * The right hand side URI to open inside a diff editor. + * The right hand side editor to open inside a diff editor. */ - readonly rightResource: URI; + readonly modifiedInput: IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput; } export const enum Verbosity { @@ -393,10 +371,39 @@ export interface IRevertOptions { } export interface IMoveResult { - editor: EditorInput | IResourceEditorInputType; + editor: IEditorInput | IResourceEditorInputType; options?: IEditorOptions; } +export const enum EditorInputCapabilities { + + /** + * Signals no specific capability for the input. + */ + None = 0, + + /** + * Signals that the input is readonly. + */ + Readonly = 1 << 1, + + /** + * Signals that the input is untitled. + */ + Untitled = 1 << 2, + + /** + * Signals that the input can only be shown in one group + * and not be split into multiple groups. + */ + Singleton = 1 << 3, + + /** + * Signals that the input requires workspace trust. + */ + RequiresTrust = 1 << 4, +} + export interface IEditorInput extends IDisposable { /** @@ -415,7 +422,12 @@ export interface IEditorInput extends IDisposable { readonly onDidChangeLabel: Event; /** - * Unique type identifier for this inpput. Every editor input of the + * Triggered when this input changes its capabilities. + */ + readonly onDidChangeCapabilities: Event; + + /** + * Unique type identifier for this input. Every editor input of the * same class should share the same type identifier. The type identifier * is used for example for serialising/deserialising editor inputs * via the serialisers of the `IEditorInputFactoryRegistry`. @@ -435,6 +447,16 @@ export interface IEditorInput extends IDisposable { */ readonly resource: URI | undefined; + /** + * The capabilities of the input. + */ + readonly capabilities: EditorInputCapabilities; + + /** + * Figure out if the input has the provided capability. + */ + hasCapability(capability: EditorInputCapabilities): boolean; + /** * Returns the display name of this input. */ @@ -462,16 +484,6 @@ export interface IEditorInput extends IDisposable { */ resolve(): Promise; - /** - * Returns if this input is readonly or not. - */ - isReadonly(): boolean; - - /** - * Returns if the input is an untitled editor or not. - */ - isUntitled(): boolean; - /** * Returns if this input is dirty or not. */ @@ -523,9 +535,18 @@ export interface IEditorInput extends IDisposable { rename(group: GroupIdentifier, target: URI): IMoveResult | undefined; /** - * Subclasses can set this to false if it does not make sense to split the editor input. + * Returns a copy of the current editor input. Used when we can't just reuse the input */ - canSplit(): boolean; + copy(): IEditorInput; + + /** + * Returns a representation of this typed editor input as untyped + * resource editor input that e.g. can be used to serialize the + * editor input into a form that it can be restored. + * + * May return `undefined` if a untyped representatin is not supported. + */ + asResourceEditorInput(groupId: GroupIdentifier): IBaseResourceEditorInput | undefined; /** * Returns if the other object matches this input. @@ -536,130 +557,6 @@ export interface IEditorInput extends IDisposable { * Returns if this editor is disposed. */ isDisposed(): boolean; - - /** - * Returns a copy of the current editor input. Used when we can't just reuse the input - */ - copy(): IEditorInput; -} - -/** - * Editor inputs are lightweight objects that can be passed to the workbench API to open inside the editor part. - * Each editor input is mapped to an editor that is capable of opening it through the Platform facade. - */ -export abstract class EditorInput extends Disposable implements IEditorInput { - - protected readonly _onDidChangeDirty = this._register(new Emitter()); - readonly onDidChangeDirty = this._onDidChangeDirty.event; - - protected readonly _onDidChangeLabel = this._register(new Emitter()); - readonly onDidChangeLabel = this._onDidChangeLabel.event; - - private readonly _onWillDispose = this._register(new Emitter()); - readonly onWillDispose = this._onWillDispose.event; - - private disposed: boolean = false; - - abstract get typeId(): string; - - abstract get resource(): URI | undefined; - - getName(): string { - return `Editor ${this.typeId}`; - } - - getDescription(verbosity?: Verbosity): string | undefined { - return undefined; - } - - getTitle(verbosity?: Verbosity): string { - return this.getName(); - } - - getAriaLabel(): string { - return this.getTitle(Verbosity.SHORT); - } - - /** - * Returns the preferred editor for this input. A list of candidate editors is passed in that whee registered - * for the input. This allows subclasses to decide late which editor to use for the input on a case by case basis. - */ - getPreferredEditorId(candidates: string[]): string | undefined { - return firstOrDefault(candidates); - } - - /** - * Returns a descriptor suitable for telemetry events. - * - * Subclasses should extend if they can contribute. - */ - getTelemetryDescriptor(): { [key: string]: unknown } { - /* __GDPR__FRAGMENT__ - "EditorTelemetryDescriptor" : { - "typeId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - return { typeId: this.typeId }; - } - - isReadonly(): boolean { - return true; - } - - isUntitled(): boolean { - return false; - } - - isDirty(): boolean { - return false; - } - - isSaving(): boolean { - return false; - } - - async resolve(): Promise { - return null; - } - - async save(group: GroupIdentifier, options?: ISaveOptions): Promise { - return this; - } - - async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { - return this; - } - - async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { } - - rename(group: GroupIdentifier, target: URI): IMoveResult | undefined { - return undefined; - } - - canSplit(): boolean { - return true; - } - - matches(otherInput: unknown): boolean { - return this === otherInput; - } - - copy(): IEditorInput { - return this; - } - - isDisposed(): boolean { - return this.disposed; - } - - override dispose(): void { - if (!this.disposed) { - this.disposed = true; - this._onWillDispose.fire(); - } - - super.dispose(); - } } export interface IEditorInputWithPreferredResource { @@ -685,9 +582,34 @@ export interface IEditorInputWithPreferredResource { } export function isEditorInputWithPreferredResource(obj: unknown): obj is IEditorInputWithPreferredResource { - const editorInputWithPreferredResource = obj as IEditorInputWithPreferredResource; + const editorInputWithPreferredResource = obj as IEditorInputWithPreferredResource | undefined; + if (!editorInputWithPreferredResource) { + return false; + } - return editorInputWithPreferredResource && !!editorInputWithPreferredResource.preferredResource; + return URI.isUri(editorInputWithPreferredResource.preferredResource); +} + +export interface ISideBySideEditorInput extends IEditorInput { + + /** + * The primary editor input is shown on the right hand side. + */ + primary: IEditorInput; + + /** + * The secondary editor input is shown on the left hand side. + */ + secondary: IEditorInput; +} + +function isSideBySideEditorInput(obj: unknown): obj is ISideBySideEditorInput { + const sideBySideEditorInput = obj as ISideBySideEditorInput | undefined; + if (!sideBySideEditorInput) { + return false; + } + + return !!sideBySideEditorInput.primary && !!sideBySideEditorInput.secondary; } /** @@ -737,6 +659,11 @@ export interface IFileEditorInput extends IEditorInput, IEncodingSupport, IModeS */ setPreferredMode(mode: string): void; + /** + * Sets the preferred contents to use for this file input. + */ + setPreferredContents(contents: string): void; + /** * Forces this file input to open as binary instead of text. */ @@ -748,173 +675,9 @@ export interface IFileEditorInput extends IEditorInput, IEncodingSupport, IModeS isResolved(): boolean; } -/** - * Side by side editor inputs that have a primary and secondary side. - */ -export class SideBySideEditorInput extends EditorInput { - - static readonly ID: string = 'workbench.editorinputs.sidebysideEditorInput'; - - override get typeId(): string { - return SideBySideEditorInput.ID; - } - - constructor( - protected readonly name: string | undefined, - protected readonly description: string | undefined, - private readonly _secondary: EditorInput, - private readonly _primary: EditorInput - ) { - super(); - - this.registerListeners(); - } - - private registerListeners(): void { - - // When the primary or secondary input gets disposed, dispose this diff editor input - const onceSecondaryDisposed = Event.once(this.secondary.onWillDispose); - this._register(onceSecondaryDisposed(() => { - if (!this.isDisposed()) { - this.dispose(); - } - })); - - const oncePrimaryDisposed = Event.once(this.primary.onWillDispose); - this._register(oncePrimaryDisposed(() => { - if (!this.isDisposed()) { - this.dispose(); - } - })); - - // Reemit some events from the primary side to the outside - this._register(this.primary.onDidChangeDirty(() => this._onDidChangeDirty.fire())); - this._register(this.primary.onDidChangeLabel(() => this._onDidChangeLabel.fire())); - } - - /** - * Use `EditorResourceAccessor` utility method to access the resources - * of both sides of the diff editor. - */ - get resource(): URI | undefined { - return undefined; - } - - get primary(): EditorInput { - return this._primary; - } - - get secondary(): EditorInput { - return this._secondary; - } - - override getName(): string { - if (!this.name) { - return localize('sideBySideLabels', "{0} - {1}", this._secondary.getName(), this._primary.getName()); - } - - return this.name; - } - - override getDescription(): string | undefined { - return this.description; - } - - override isReadonly(): boolean { - return this.primary.isReadonly(); - } - - override isUntitled(): boolean { - return this.primary.isUntitled(); - } - - override isDirty(): boolean { - return this.primary.isDirty(); - } - - override isSaving(): boolean { - return this.primary.isSaving(); - } - - override save(group: GroupIdentifier, options?: ISaveOptions): Promise { - return this.primary.save(group, options); - } - - override saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { - return this.primary.saveAs(group, options); - } - - override revert(group: GroupIdentifier, options?: IRevertOptions): Promise { - return this.primary.revert(group, options); - } - - override getTelemetryDescriptor(): { [key: string]: unknown } { - const descriptor = this.primary.getTelemetryDescriptor(); - - return Object.assign(descriptor, super.getTelemetryDescriptor()); - } - - override matches(otherInput: unknown): boolean { - if (otherInput === this) { - return true; - } - - if (otherInput instanceof SideBySideEditorInput) { - return this.primary.matches(otherInput.primary) && this.secondary.matches(otherInput.secondary); - } - - return false; - } -} - -/** - * The editor model is the heavyweight counterpart of editor input. Depending on the editor input, it - * resolves from a file system retrieve content and may allow for saving it back or reverting it. - * Editor models are typically cached for some while because they are expensive to construct. - */ -export class EditorModel extends Disposable implements IEditorModel { - - private readonly _onWillDispose = this._register(new Emitter()); - readonly onWillDispose = this._onWillDispose.event; - - private disposed = false; - private resolved = false; - - /** - * Causes this model to resolve returning a promise when loading is completed. - */ - async resolve(): Promise { - this.resolved = true; - } - - /** - * Returns whether this model was loaded or not. - */ - isResolved(): boolean { - return this.resolved; - } - - /** - * Find out if this model has been disposed. - */ - isDisposed(): boolean { - return this.disposed; - } - - /** - * Subclasses should implement to free resources that have been claimed through loading. - */ - override dispose(): void { - this.disposed = true; - this._onWillDispose.fire(); - - super.dispose(); - } -} - export interface IEditorInputWithOptions { editor: IEditorInput; - options?: IEditorOptions | ITextEditorOptions; + options?: IEditorOptions; } export interface IEditorInputWithOptionsAndGroup extends IEditorInputWithOptions { @@ -927,289 +690,6 @@ export function isEditorInputWithOptions(obj: unknown): obj is IEditorInputWithO return !!editorInputWithOptions && !!editorInputWithOptions.editor; } -/** - * The editor options is the base class of options that can be passed in when opening an editor. - */ -export class EditorOptions implements IEditorOptions { - - /** - * Helper to create EditorOptions inline. - */ - static create(settings: IEditorOptions): EditorOptions { - const options = new EditorOptions(); - options.overwrite(settings); - - return options; - } - - /** - * Tells the editor to not receive keyboard focus when the editor is being opened. - * - * Will also not activate the group the editor opens in unless the group is already - * the active one. This behaviour can be overridden via the `activation` option. - */ - preserveFocus: boolean | undefined; - - /** - * This option is only relevant if an editor is opened into a group that is not active - * already and allows to control if the inactive group should become active, restored - * or preserved. - * - * By default, the editor group will become active unless `preserveFocus` or `inactive` - * is specified. - */ - activation: EditorActivation | undefined; - - /** - * Tells the editor to reload the editor input in the editor even if it is identical to the one - * already showing. By default, the editor will not reload the input if it is identical to the - * one showing. - */ - forceReload: boolean | undefined; - - /** - * Will reveal the editor if it is already opened and visible in any of the opened editor groups. - */ - revealIfVisible: boolean | undefined; - - /** - * Will reveal the editor if it is already opened (even when not visible) in any of the opened editor groups. - */ - revealIfOpened: boolean | undefined; - - /** - * An editor that is pinned remains in the editor stack even when another editor is being opened. - * An editor that is not pinned will always get replaced by another editor that is not pinned. - */ - pinned: boolean | undefined; - - /** - * An editor that is sticky moves to the beginning of the editors list within the group and will remain - * there unless explicitly closed. Operations such as "Close All" will not close sticky editors. - */ - sticky: boolean | undefined; - - /** - * The index in the document stack where to insert the editor into when opening. - */ - index: number | undefined; - - /** - * An active editor that is opened will show its contents directly. Set to true to open an editor - * in the background without loading its contents. - * - * Will also not activate the group the editor opens in unless the group is already - * the active one. This behaviour can be overridden via the `activation` option. - */ - inactive: boolean | undefined; - - /** - * Will not show an error in case opening the editor fails and thus allows to show a custom error - * message as needed. By default, an error will be presented as notification if opening was not possible. - */ - ignoreError: boolean | undefined; - - /** - * Allows to override the editor that should be used to display the input: - * - `undefined`: let the editor decide for itself - * - `string`: specific override by id - * - `EditorOverride`: specific override handling - */ - override: string | EditorOverride | undefined; - - /** - * A optional hint to signal in which context the editor opens. - * - * If configured to be `EditorOpenContext.USER`, this hint can be - * used in various places to control the experience. For example, - * if the editor to open fails with an error, a notification could - * inform about this in a modal dialog. If the editor opened through - * some background task, the notification would show in the background, - * not as a modal dialog. - */ - context: EditorOpenContext | undefined; - - /** - * Overwrites option values from the provided bag. - */ - overwrite(options: IEditorOptions): EditorOptions { - if (typeof options.forceReload === 'boolean') { - this.forceReload = options.forceReload; - } - - if (typeof options.revealIfVisible === 'boolean') { - this.revealIfVisible = options.revealIfVisible; - } - - if (typeof options.revealIfOpened === 'boolean') { - this.revealIfOpened = options.revealIfOpened; - } - - if (typeof options.preserveFocus === 'boolean') { - this.preserveFocus = options.preserveFocus; - } - - if (typeof options.activation === 'number') { - this.activation = options.activation; - } - - if (typeof options.pinned === 'boolean') { - this.pinned = options.pinned; - } - - if (typeof options.sticky === 'boolean') { - this.sticky = options.sticky; - } - - if (typeof options.inactive === 'boolean') { - this.inactive = options.inactive; - } - - if (typeof options.ignoreError === 'boolean') { - this.ignoreError = options.ignoreError; - } - - if (typeof options.index === 'number') { - this.index = options.index; - } - - if (options.override !== undefined) { - this.override = options.override; - } - - if (typeof options.context === 'number') { - this.context = options.context; - } - - return this; - } -} - -/** - * Base Text Editor Options. - */ -export class TextEditorOptions extends EditorOptions implements ITextEditorOptions { - - /** - * Text editor selection. - */ - selection: ITextEditorSelection | undefined; - - /** - * Text editor view state. - */ - editorViewState: IEditorViewState | undefined; - - /** - * Option to control the text editor selection reveal type. - */ - selectionRevealType: TextEditorSelectionRevealType | undefined; - - static from(input?: IBaseResourceEditorInput): TextEditorOptions | undefined { - if (!input?.options) { - return undefined; - } - - return TextEditorOptions.create(input.options); - } - - /** - * Helper to convert options bag to real class - */ - static override create(options: ITextEditorOptions = Object.create(null)): TextEditorOptions { - const textEditorOptions = new TextEditorOptions(); - textEditorOptions.overwrite(options); - - return textEditorOptions; - } - - /** - * Overwrites option values from the provided bag. - */ - override overwrite(options: ITextEditorOptions): TextEditorOptions { - super.overwrite(options); - - if (options.selection) { - this.selection = { - startLineNumber: options.selection.startLineNumber, - startColumn: options.selection.startColumn, - endLineNumber: options.selection.endLineNumber ?? options.selection.startLineNumber, - endColumn: options.selection.endColumn ?? options.selection.startColumn - }; - } - - if (options.viewState) { - this.editorViewState = options.viewState as IEditorViewState; - } - - if (typeof options.selectionRevealType !== 'undefined') { - this.selectionRevealType = options.selectionRevealType; - } - - return this; - } - - /** - * Returns if this options object has objects defined for the editor. - */ - hasOptionsDefined(): boolean { - return !!this.editorViewState || !!this.selectionRevealType || !!this.selection; - } - - /** - * Create a TextEditorOptions inline to be used when the editor is opening. - */ - static fromEditor(editor: IEditor, settings?: IEditorOptions): TextEditorOptions { - const options = TextEditorOptions.create(settings); - - // View state - options.editorViewState = withNullAsUndefined(editor.saveViewState()); - - return options; - } - - /** - * Apply the view state or selection to the given editor. - * - * @return if something was applied - */ - apply(editor: IEditor, scrollType: ScrollType): boolean { - let gotApplied = false; - - // First try viewstate - if (this.editorViewState) { - editor.restoreViewState(this.editorViewState); - gotApplied = true; - } - - // Otherwise check for selection - else if (this.selection) { - const range: IRange = { - startLineNumber: this.selection.startLineNumber, - startColumn: this.selection.startColumn, - endLineNumber: this.selection.endLineNumber ?? this.selection.startLineNumber, - endColumn: this.selection.endColumn ?? this.selection.startColumn - }; - - editor.setSelection(range); - - if (this.selectionRevealType === TextEditorSelectionRevealType.NearTop) { - editor.revealRangeNearTop(range, scrollType); - } else if (this.selectionRevealType === TextEditorSelectionRevealType.NearTopIfOutsideViewport) { - editor.revealRangeNearTopIfOutsideViewport(range, scrollType); - } else if (this.selectionRevealType === TextEditorSelectionRevealType.CenterIfOutsideViewport) { - editor.revealRangeInCenterIfOutsideViewport(range, scrollType); - } else { - editor.revealRangeInCenter(range, scrollType); - } - - gotApplied = true; - } - - return gotApplied; - } -} - /** * Context passed into `EditorPane#setInput` to give additional * context information around why the editor was opened. @@ -1231,6 +711,15 @@ export interface IEditorIdentifier { editor: IEditorInput; } +export function isEditorIdentifier(thing: unknown): thing is IEditorIdentifier { + const identifier = thing as IEditorIdentifier | undefined; + if (!identifier) { + return false; + } + + return typeof identifier.groupId === 'number' && !isUndefinedOrNull(identifier.editor); +} + /** * The editor commands context is used for editor commands (e.g. in the editor title) * and we must ensure that the context is serializable because it potentially travels @@ -1241,19 +730,6 @@ export interface IEditorCommandsContext { editorIndex?: number; } -export class EditorCommandsContextActionRunner extends ActionRunner { - - constructor( - private context: IEditorCommandsContext - ) { - super(); - } - - override run(action: IAction): Promise { - return super.run(action, this.context); - } -} - export interface IEditorCloseEvent extends IEditorIdentifier { replaced: boolean; index: number; @@ -1264,6 +740,8 @@ export interface IEditorMoveEvent extends IEditorIdentifier { target: GroupIdentifier; } +export interface IEditorOpenEvent extends IEditorIdentifier { } + export type GroupIdentifier = number; export interface IWorkbenchEditorConfiguration { @@ -1366,7 +844,7 @@ class EditorResourceAccessorImpl { } // Optionally support side-by-side editors - if (options?.supportSideBySide && editor instanceof SideBySideEditorInput) { + if (options?.supportSideBySide && isSideBySideEditorInput(editor)) { if (options?.supportSideBySide === SideBySideEditor.BOTH) { return { primary: this.getOriginalUri(editor.primary, { filterByScheme: options.filterByScheme }), @@ -1408,7 +886,7 @@ class EditorResourceAccessorImpl { } // Optionally support side-by-side editors - if (options?.supportSideBySide && editor instanceof SideBySideEditorInput) { + if (options?.supportSideBySide && isSideBySideEditorInput(editor)) { if (options?.supportSideBySide === SideBySideEditor.BOTH) { return { primary: this.getCanonicalUri(editor.primary, { filterByScheme: options.filterByScheme }), @@ -1475,7 +953,6 @@ class EditorInputFactoryRegistry implements IEditorInputFactoryRegistry { private instantiationService: IInstantiationService | undefined; private fileEditorInputFactory: IFileEditorInputFactory | undefined; - private readonly customEditorInputFactoryInstances: Map = new Map(); private readonly editorInputSerializerConstructors: Map> = new Map(); private readonly editorInputSerializerInstances: Map = new Map(); @@ -1529,14 +1006,6 @@ class EditorInputFactoryRegistry implements IEditorInputFactoryRegistry { getEditorInputSerializer(arg1: string | IEditorInput): IEditorInputSerializer | undefined { return this.editorInputSerializerInstances.get(typeof arg1 === 'string' ? arg1 : arg1.typeId); } - - registerCustomEditorInputFactory(scheme: string, factory: ICustomEditorInputFactory): void { - this.customEditorInputFactoryInstances.set(scheme, factory); - } - - getCustomEditorInputFactory(scheme: string): ICustomEditorInputFactory | undefined { - return this.customEditorInputFactoryInstances.get(scheme); - } } Registry.add(EditorExtensions.EditorInputFactories, new EditorInputFactoryRegistry()); diff --git a/src/vs/workbench/common/editor/binaryEditorModel.ts b/src/vs/workbench/common/editor/binaryEditorModel.ts index a675446a19..027b820dff 100644 --- a/src/vs/workbench/common/editor/binaryEditorModel.ts +++ b/src/vs/workbench/common/editor/binaryEditorModel.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorModel } from 'vs/workbench/common/editor'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; import { MIME_BINARY } from 'vs/base/common/mime'; @@ -12,20 +12,18 @@ import { MIME_BINARY } from 'vs/base/common/mime'; * An editor model that just represents a resource that can be loaded. */ export class BinaryEditorModel extends EditorModel { + + private readonly mime = MIME_BINARY; + private size: number | undefined; private etag: string | undefined; - private readonly mime: string; constructor( - public readonly resource: URI, + readonly resource: URI, private readonly name: string, @IFileService private readonly fileService: IFileService ) { super(); - - this.resource = resource; - this.name = name; - this.mime = MIME_BINARY; } /** diff --git a/src/vs/workbench/common/editor/diffEditorInput.ts b/src/vs/workbench/common/editor/diffEditorInput.ts index c9698fefae..13dc2feddf 100644 --- a/src/vs/workbench/common/editor/diffEditorInput.ts +++ b/src/vs/workbench/common/editor/diffEditorInput.ts @@ -3,7 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorModel, EditorInput, SideBySideEditorInput, TEXT_DIFF_EDITOR_ID, BINARY_DIFF_EDITOR_ID, Verbosity } from 'vs/workbench/common/editor'; +import { AbstractSideBySideEditorInputSerializer, SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; +import { TEXT_DIFF_EDITOR_ID, BINARY_DIFF_EDITOR_ID, Verbosity, IEditorDescriptor, IEditorPane, GroupIdentifier, IResourceDiffEditorInput } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { DiffEditorModel } from 'vs/workbench/common/editor/diffEditorModel'; import { TextDiffEditorModel } from 'vs/workbench/common/editor/textDiffEditorModel'; @@ -14,6 +17,7 @@ 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'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; /** * The base editor input for the diff editor. It is made up of two editor inputs, the original version @@ -32,8 +36,8 @@ export class DiffEditorInput extends SideBySideEditorInput { constructor( name: string | undefined, description: string | undefined, - public readonly originalInput: EditorInput, - public readonly modifiedInput: EditorInput, + readonly originalInput: EditorInput, + readonly modifiedInput: EditorInput, private readonly forceOpenAsBinary: boolean | undefined, @ILabelService private readonly labelService: ILabelService, @IFileService private readonly fileService: IFileService @@ -58,7 +62,7 @@ export class DiffEditorInput extends SideBySideEditorInput { return this.name; } - override getDescription(verbosity: Verbosity = Verbosity.MEDIUM): string | undefined { + override getDescription(verbosity = Verbosity.MEDIUM): string | undefined { if (typeof this.description !== 'string') { // Pass the description of the modified side in case both original @@ -104,8 +108,12 @@ export class DiffEditorInput extends SideBySideEditorInput { return this.cachedModel; } - override getPreferredEditorId(candidates: string[]): string { - return this.forceOpenAsBinary ? BINARY_DIFF_EDITOR_ID : TEXT_DIFF_EDITOR_ID; + override prefersEditor>(editors: T[]): T | undefined { + if (this.forceOpenAsBinary) { + return editors.find(editor => editor.typeId === BINARY_DIFF_EDITOR_ID); + } + + return editors.find(editor => editor.typeId === TEXT_DIFF_EDITOR_ID); } private async createModel(): Promise { @@ -125,6 +133,22 @@ export class DiffEditorInput extends SideBySideEditorInput { return new DiffEditorModel(withNullAsUndefined(originalEditorModel), withNullAsUndefined(modifiedEditorModel)); } + override asResourceEditorInput(groupId: GroupIdentifier): IResourceDiffEditorInput | undefined { + const originalResourceEditorInput = this.secondary.asResourceEditorInput(groupId); + const modifiedResourceEditorInput = this.primary.asResourceEditorInput(groupId); + + if (originalResourceEditorInput && modifiedResourceEditorInput) { + return { + label: this.name, + description: this.description, + originalInput: originalResourceEditorInput, + modifiedInput: modifiedResourceEditorInput + }; + } + + return undefined; + } + override matches(otherInput: unknown): boolean { if (!super.matches(otherInput)) { return false; @@ -146,3 +170,10 @@ export class DiffEditorInput extends SideBySideEditorInput { super.dispose(); } } + +export class DiffEditorInputSerializer extends AbstractSideBySideEditorInputSerializer { + + protected createEditorInput(instantiationService: IInstantiationService, name: string, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput { + return instantiationService.createInstance(DiffEditorInput, name, description, secondaryInput, primaryInput, undefined); + } +} diff --git a/src/vs/workbench/common/editor/diffEditorModel.ts b/src/vs/workbench/common/editor/diffEditorModel.ts index 58c6489bfc..666a24f56f 100644 --- a/src/vs/workbench/common/editor/diffEditorModel.ts +++ b/src/vs/workbench/common/editor/diffEditorModel.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorModel } from 'vs/workbench/common/editor'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { IEditorModel } from 'vs/platform/editor/common/editor'; /** diff --git a/src/vs/workbench/common/editor/editorGroupModel.ts b/src/vs/workbench/common/editor/editorGroupModel.ts index b3e5c4aa70..3158d47163 100644 --- a/src/vs/workbench/common/editor/editorGroupModel.ts +++ b/src/vs/workbench/common/editor/editorGroupModel.ts @@ -4,13 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Emitter } from 'vs/base/common/event'; -import { IEditorInputFactoryRegistry, EditorInput, IEditorIdentifier, IEditorCloseEvent, GroupIdentifier, SideBySideEditorInput, IEditorInput, EditorsOrder, EditorExtensions } from 'vs/workbench/common/editor'; +import { IEditorInputFactoryRegistry, IEditorIdentifier, IEditorCloseEvent, GroupIdentifier, IEditorInput, EditorsOrder, EditorExtensions } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { coalesce } from 'vs/base/common/arrays'; -import { doHandleUpgrade } from 'sql/workbench/services/languageAssociation/common/doHandleUpgrade'; const EditorOpenPositioning = { LEFT: 'left', @@ -81,7 +82,10 @@ export class EditorGroupModel extends Disposable { readonly onDidChangeEditorDirty = this._onDidChangeEditorDirty.event; private readonly _onDidChangeEditorLabel = this._register(new Emitter()); - readonly onDidEditorLabelChange = this._onDidChangeEditorLabel.event; + readonly onDidChangeEditorLabel = this._onDidChangeEditorLabel.event; + + private readonly _onDidChangeEditorCapabilities = this._register(new Emitter()); + readonly onDidChangeEditorCapabilities = this._onDidChangeEditorCapabilities.event; private readonly _onDidMoveEditor = this._register(new Emitter()); readonly onDidMoveEditor = this._onDidMoveEditor.event; @@ -332,6 +336,11 @@ export class EditorGroupModel extends Disposable { this._onDidChangeEditorLabel.fire(editor); })); + // Re-Emit capability changes + listeners.add(editor.onDidChangeCapabilities(() => { + this._onDidChangeEditorCapabilities.fire(editor); + })); + // Clean up dispose listeners once the editor gets closed listeners.add(this.onDidCloseEditor(event => { if (event.editor.matches(editor)) { @@ -811,8 +820,9 @@ export class EditorGroupModel extends Disposable { const editorSerializer = registry.getEditorInputSerializer(e.id); if (editorSerializer) { - editor = doHandleUpgrade(editorSerializer.deserialize(this.instantiationService, e.value)); // {{SQL CARBON EDIT}} handle upgrade path to new serialization - if (editor) { + const deserializedEditor = editorSerializer.deserialize(this.instantiationService, e.value); + if (deserializedEditor instanceof EditorInput) { + editor = deserializedEditor; this.registerEditorListeners(editor); } } diff --git a/src/vs/workbench/common/editor/editorInput.ts b/src/vs/workbench/common/editor/editorInput.ts new file mode 100644 index 0000000000..a3bc843783 --- /dev/null +++ b/src/vs/workbench/common/editor/editorInput.ts @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { URI } from 'vs/base/common/uri'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IBaseResourceEditorInput, IEditorModel } from 'vs/platform/editor/common/editor'; +import { firstOrDefault } from 'vs/base/common/arrays'; +import { IEditorInput, EditorInputCapabilities, Verbosity, GroupIdentifier, ISaveOptions, IRevertOptions, IMoveResult, IEditorDescriptor, IEditorPane } from 'vs/workbench/common/editor'; + +/** + * Editor inputs are lightweight objects that can be passed to the workbench API to open inside the editor part. + * Each editor input is mapped to an editor that is capable of opening it through the Platform facade. + */ +export abstract class EditorInput extends Disposable implements IEditorInput { + + protected readonly _onDidChangeDirty = this._register(new Emitter()); + readonly onDidChangeDirty = this._onDidChangeDirty.event; + + protected readonly _onDidChangeLabel = this._register(new Emitter()); + readonly onDidChangeLabel = this._onDidChangeLabel.event; + + protected readonly _onDidChangeCapabilities = this._register(new Emitter()); + readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event; + + private readonly _onWillDispose = this._register(new Emitter()); + readonly onWillDispose = this._onWillDispose.event; + + private disposed: boolean = false; + + abstract get typeId(): string; + + abstract get resource(): URI | undefined; + + get capabilities(): EditorInputCapabilities { + return EditorInputCapabilities.Readonly; + } + + hasCapability(capability: EditorInputCapabilities): boolean { + if (capability === EditorInputCapabilities.None) { + return this.capabilities === EditorInputCapabilities.None; + } + + return (this.capabilities & capability) !== 0; + } + + getName(): string { + return `Editor ${this.typeId}`; + } + + getDescription(verbosity?: Verbosity): string | undefined { + return undefined; + } + + getTitle(verbosity?: Verbosity): string { + return this.getName(); + } + + getAriaLabel(): string { + return this.getTitle(Verbosity.SHORT); + } + + /** + * Returns a descriptor suitable for telemetry events. + * + * Subclasses should extend if they can contribute. + */ + getTelemetryDescriptor(): { [key: string]: unknown } { + /* __GDPR__FRAGMENT__ + "EditorTelemetryDescriptor" : { + "typeId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + return { typeId: this.typeId }; + } + + isDirty(): boolean { + return false; + } + + isSaving(): boolean { + return false; + } + + async resolve(): Promise { + return null; + } + + async save(group: GroupIdentifier, options?: ISaveOptions): Promise { + return this; + } + + async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { + return this; + } + + async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { } + + rename(group: GroupIdentifier, target: URI): IMoveResult | undefined { + return undefined; + } + + copy(): IEditorInput { + return this; + } + + matches(otherInput: unknown): boolean { + return this === otherInput; + } + + /** + * If a input was registered onto multiple editors, this method + * will be asked to return the preferred one to use. + * + * @param editors a list of editor descriptors that are candidates + * for the editor input to open in. + */ + prefersEditor>(editors: T[]): T | undefined { + return firstOrDefault(editors); + } + + asResourceEditorInput(groupId: GroupIdentifier): IBaseResourceEditorInput | undefined { + return undefined; + } + + isDisposed(): boolean { + return this.disposed; + } + + override dispose(): void { + if (!this.disposed) { + this.disposed = true; + this._onWillDispose.fire(); + } + + super.dispose(); + } +} diff --git a/src/vs/workbench/common/editor/editorModel.ts b/src/vs/workbench/common/editor/editorModel.ts new file mode 100644 index 0000000000..d1c18e9986 --- /dev/null +++ b/src/vs/workbench/common/editor/editorModel.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IEditorModel } from 'vs/platform/editor/common/editor'; + +/** + * The editor model is the heavyweight counterpart of editor input. Depending on the editor input, it + * resolves from a file system retrieve content and may allow for saving it back or reverting it. + * Editor models are typically cached for some while because they are expensive to construct. + */ +export class EditorModel extends Disposable implements IEditorModel { + + private readonly _onWillDispose = this._register(new Emitter()); + readonly onWillDispose = this._onWillDispose.event; + + private disposed = false; + private resolved = false; + + /** + * Causes this model to resolve returning a promise when loading is completed. + */ + async resolve(): Promise { + this.resolved = true; + } + + /** + * Returns whether this model was loaded or not. + */ + isResolved(): boolean { + return this.resolved; + } + + /** + * Find out if this model has been disposed. + */ + isDisposed(): boolean { + return this.disposed; + } + + /** + * Subclasses should implement to free resources that have been claimed through loading. + */ + override dispose(): void { + this.disposed = true; + this._onWillDispose.fire(); + + super.dispose(); + } +} diff --git a/src/vs/workbench/common/editor/editorOptions.ts b/src/vs/workbench/common/editor/editorOptions.ts new file mode 100644 index 0000000000..2fb300242e --- /dev/null +++ b/src/vs/workbench/common/editor/editorOptions.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IRange } from 'vs/editor/common/core/range'; +import { IEditor, IEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'; +import { ITextEditorOptions, TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; + +export function applyTextEditorOptions(options: ITextEditorOptions, editor: IEditor, scrollType: ScrollType): boolean { + + // First try viewstate + if (options.viewState) { + editor.restoreViewState(options.viewState as IEditorViewState); + + return true; + } + + // Otherwise check for selection + else if (options.selection) { + const range: IRange = { + startLineNumber: options.selection.startLineNumber, + startColumn: options.selection.startColumn, + endLineNumber: options.selection.endLineNumber ?? options.selection.startLineNumber, + endColumn: options.selection.endColumn ?? options.selection.startColumn + }; + + editor.setSelection(range); + + if (options.selectionRevealType === TextEditorSelectionRevealType.NearTop) { + editor.revealRangeNearTop(range, scrollType); + } else if (options.selectionRevealType === TextEditorSelectionRevealType.NearTopIfOutsideViewport) { + editor.revealRangeNearTopIfOutsideViewport(range, scrollType); + } else if (options.selectionRevealType === TextEditorSelectionRevealType.CenterIfOutsideViewport) { + editor.revealRangeInCenterIfOutsideViewport(range, scrollType); + } else { + editor.revealRangeInCenter(range, scrollType); + } + + return true; + } + + return false; +} diff --git a/src/vs/workbench/common/editor/resourceEditorInput.ts b/src/vs/workbench/common/editor/resourceEditorInput.ts index e5859f0abf..e293dfd99b 100644 --- a/src/vs/workbench/common/editor/resourceEditorInput.ts +++ b/src/vs/workbench/common/editor/resourceEditorInput.ts @@ -3,132 +3,197 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; +import { Verbosity, IEditorInputWithPreferredResource, EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { URI } from 'vs/base/common/uri'; -import { IReference } from 'vs/base/common/lifecycle'; -import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; -import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; -import { IModeSupport, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IFileService } from 'vs/platform/files/common/files'; +import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; -import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; -import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; -import { isEqual } from 'vs/base/common/resources'; +import { dirname, isEqual } from 'vs/base/common/resources'; /** - * A read-only text editor input whos contents are made of the provided resource that points to an existing - * code editor model. + * The base class for all editor inputs that open resources. */ -export class ResourceEditorInput extends AbstractTextResourceEditorInput implements IModeSupport { +export abstract class AbstractResourceEditorInput extends EditorInput implements IEditorInputWithPreferredResource { - static readonly ID: string = 'workbench.editors.resourceEditorInput'; + override get capabilities(): EditorInputCapabilities { + let capabilities = EditorInputCapabilities.None; - override get typeId(): string { - return ResourceEditorInput.ID; + if (this.fileService.canHandleResource(this.resource)) { + if (this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly)) { + capabilities |= EditorInputCapabilities.Readonly; + } + } else { + capabilities |= EditorInputCapabilities.Untitled; + } + + return capabilities; } - private cachedModel: ResourceEditorModel | undefined = undefined; - private modelReference: Promise> | undefined = undefined; + private _preferredResource: URI; + get preferredResource(): URI { return this._preferredResource; } constructor( - resource: URI, - private name: string | undefined, - private description: string | undefined, - private preferredMode: string | undefined, - @ITextModelService private readonly textModelResolverService: ITextModelService, - @ITextFileService textFileService: ITextFileService, - @IEditorService editorService: IEditorService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IFileService fileService: IFileService, - @ILabelService labelService: ILabelService, - @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService + readonly resource: URI, + preferredResource: URI | undefined, + @ILabelService protected readonly labelService: ILabelService, + @IFileService protected readonly fileService: IFileService ) { - super(resource, undefined, editorService, editorGroupService, textFileService, labelService, fileService, filesConfigurationService); + super(); + + this._preferredResource = preferredResource || resource; + + this.registerListeners(); } - override getName(): string { - return this.name || super.getName(); + private registerListeners(): void { + + // Clear our labels on certain label related events + this._register(this.labelService.onDidChangeFormatters(e => this.onLabelEvent(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onLabelEvent(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onLabelEvent(e.scheme))); } - setName(name: string): void { - if (this.name !== name) { - this.name = name; - - this._onDidChangeLabel.fire(); + private onLabelEvent(scheme: string): void { + if (scheme === this._preferredResource.scheme) { + this.updateLabel(); } } - override getDescription(): string | undefined { - return this.description; + private updateLabel(): void { + + // Clear any cached labels from before + this._name = undefined; + this._shortDescription = undefined; + this._mediumDescription = undefined; + this._longDescription = undefined; + this._shortTitle = undefined; + this._mediumTitle = undefined; + this._longTitle = undefined; + + // Trigger recompute of label + this._onDidChangeLabel.fire(); } - setDescription(description: string): void { - if (this.description !== description) { - this.description = description; + setPreferredResource(preferredResource: URI): void { + if (!isEqual(preferredResource, this._preferredResource)) { + this._preferredResource = preferredResource; - this._onDidChangeLabel.fire(); + this.updateLabel(); } } - setMode(mode: string): void { - this.setPreferredMode(mode); + private _name: string | undefined = undefined; + override getName(skipDecorate?: boolean): string { + if (typeof this._name !== 'string') { + this._name = this.labelService.getUriBasenameLabel(this._preferredResource); + } - if (this.cachedModel) { - this.cachedModel.setMode(mode); + return skipDecorate ? this._name : this.decorateLabel(this._name); + } + + override getDescription(verbosity = Verbosity.MEDIUM): string | undefined { + switch (verbosity) { + case Verbosity.SHORT: + return this.shortDescription; + case Verbosity.LONG: + return this.longDescription; + case Verbosity.MEDIUM: + default: + return this.mediumDescription; } } - setPreferredMode(mode: string): void { - this.preferredMode = mode; + private _shortDescription: string | undefined = undefined; + private get shortDescription(): string { + if (typeof this._shortDescription !== 'string') { + this._shortDescription = this.labelService.getUriBasenameLabel(dirname(this._preferredResource)); + } + + return this._shortDescription; } - override async resolve(): Promise { - if (!this.modelReference) { - this.modelReference = this.textModelResolverService.createModelReference(this.resource); + private _mediumDescription: string | undefined = undefined; + private get mediumDescription(): string { + if (typeof this._mediumDescription !== 'string') { + this._mediumDescription = this.labelService.getUriLabel(dirname(this._preferredResource), { relative: true }); } - const ref = await this.modelReference; - - // Ensure the resolved model is of expected type - const model = ref.object; - if (!(model instanceof ResourceEditorModel)) { - ref.dispose(); - this.modelReference = undefined; - - throw new Error(`Unexpected model for ResourceEditorInput: ${this.resource}`); - } - - this.cachedModel = model; - - // Set mode if we have a preferred mode configured - if (this.preferredMode) { - model.setMode(this.preferredMode); - } - - return model; + return this._mediumDescription; } - override matches(otherInput: unknown): boolean { - if (otherInput === this) { - return true; + private _longDescription: string | undefined = undefined; + private get longDescription(): string { + if (typeof this._longDescription !== 'string') { + this._longDescription = this.labelService.getUriLabel(dirname(this._preferredResource)); } - if (otherInput instanceof ResourceEditorInput) { - return isEqual(otherInput.resource, this.resource); + return this._longDescription; + } + + private _shortTitle: string | undefined = undefined; + private get shortTitle(): string { + if (typeof this._shortTitle !== 'string') { + this._shortTitle = this.getName(true /* skip decorations */); } + return this._shortTitle; + } + + private _mediumTitle: string | undefined = undefined; + private get mediumTitle(): string { + if (typeof this._mediumTitle !== 'string') { + this._mediumTitle = this.labelService.getUriLabel(this._preferredResource, { relative: true }); + } + + return this._mediumTitle; + } + + private _longTitle: string | undefined = undefined; + private get longTitle(): string { + if (typeof this._longTitle !== 'string') { + this._longTitle = this.labelService.getUriLabel(this._preferredResource); + } + + return this._longTitle; + } + + override getTitle(verbosity?: Verbosity): string { + switch (verbosity) { + case Verbosity.SHORT: + return this.decorateLabel(this.shortTitle); + case Verbosity.LONG: + return this.decorateLabel(this.longTitle); + default: + case Verbosity.MEDIUM: + return this.decorateLabel(this.mediumTitle); + } + } + + private decorateLabel(label: string): string { + const readonly = this.hasCapability(EditorInputCapabilities.Readonly); + const orphaned = this.isOrphaned(); + + return decorateFileEditorLabel(label, { orphaned, readonly }); + } + + isOrphaned(): boolean { return false; } - - override dispose(): void { - if (this.modelReference) { - this.modelReference.then(ref => ref.dispose()); - this.modelReference = undefined; - } - - this.cachedModel = undefined; - - super.dispose(); - } +} + +export function decorateFileEditorLabel(label: string, state: { orphaned: boolean, readonly: boolean }): string { + if (state.orphaned && state.readonly) { + return localize('orphanedReadonlyFile', "{0} (deleted, read-only)", label); + } + + if (state.orphaned) { + return localize('orphanedFile', "{0} (deleted)", label); + } + + if (state.readonly) { + return localize('readonlyFile', "{0} (read-only)", label); + } + + return label; } diff --git a/src/vs/workbench/common/editor/sideBySideEditorInput.ts b/src/vs/workbench/common/editor/sideBySideEditorInput.ts new file mode 100644 index 0000000000..83e8618944 --- /dev/null +++ b/src/vs/workbench/common/editor/sideBySideEditorInput.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 { Event } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IEditorInput, EditorInputCapabilities, GroupIdentifier, ISaveOptions, IRevertOptions, EditorExtensions, IEditorInputFactoryRegistry, IEditorInputSerializer, ISideBySideEditorInput } from 'vs/workbench/common/editor'; +import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; + +/** + * Side by side editor inputs that have a primary and secondary side. + */ +export class SideBySideEditorInput extends EditorInput implements ISideBySideEditorInput { + + static readonly ID: string = 'workbench.editorinputs.sidebysideEditorInput'; + + override get typeId(): string { + return SideBySideEditorInput.ID; + } + + override get capabilities(): EditorInputCapabilities { + + // Use primary capabilities as main capabilities + let capabilities = this._primary.capabilities; + + // Trust: should be considered for both sides + if (this._secondary.hasCapability(EditorInputCapabilities.RequiresTrust)) { + capabilities |= EditorInputCapabilities.RequiresTrust; + } + + // Singleton: should be considered for both sides + if (this._secondary.hasCapability(EditorInputCapabilities.Singleton)) { + capabilities |= EditorInputCapabilities.Singleton; + } + + return capabilities; + } + + get resource(): URI | undefined { + return undefined; // use `EditorResourceAccessor` to obtain one side's resource + } + + get primary(): EditorInput { + return this._primary; + } + + get secondary(): EditorInput { + return this._secondary; + } + + constructor( + protected readonly name: string | undefined, + protected readonly description: string | undefined, + private readonly _secondary: EditorInput, + private readonly _primary: EditorInput + ) { + super(); + + this.registerListeners(); + } + + private registerListeners(): void { + + // When the primary or secondary input gets disposed, dispose this diff editor input + const onceSecondaryDisposed = Event.once(this.secondary.onWillDispose); + this._register(onceSecondaryDisposed(() => { + if (!this.isDisposed()) { + this.dispose(); + } + })); + + const oncePrimaryDisposed = Event.once(this.primary.onWillDispose); + this._register(oncePrimaryDisposed(() => { + if (!this.isDisposed()) { + this.dispose(); + } + })); + + // Re-emit some events from the primary side to the outside + this._register(this.primary.onDidChangeDirty(() => this._onDidChangeDirty.fire())); + this._register(this.primary.onDidChangeLabel(() => this._onDidChangeLabel.fire())); + + // Re-emit some events from both sides to the outside + this._register(this.primary.onDidChangeCapabilities(() => this._onDidChangeCapabilities.fire())); + this._register(this.secondary.onDidChangeCapabilities(() => this._onDidChangeCapabilities.fire())); + } + + override getName(): string { + if (!this.name) { + return localize('sideBySideLabels', "{0} - {1}", this._secondary.getName(), this._primary.getName()); + } + + return this.name; + } + + override getDescription(): string | undefined { + return this.description; + } + + override getTelemetryDescriptor(): { [key: string]: unknown } { + const descriptor = this.primary.getTelemetryDescriptor(); + + return { ...descriptor, ...super.getTelemetryDescriptor() }; + } + + override isDirty(): boolean { + return this.primary.isDirty(); + } + + override isSaving(): boolean { + return this.primary.isSaving(); + } + + override save(group: GroupIdentifier, options?: ISaveOptions): Promise { + return this.primary.save(group, options); + } + + override saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { + return this.primary.saveAs(group, options); + } + + override revert(group: GroupIdentifier, options?: IRevertOptions): Promise { + return this.primary.revert(group, options); + } + + override matches(otherInput: unknown): boolean { + if (super.matches(otherInput)) { + return true; + } + + if (otherInput instanceof SideBySideEditorInput) { + return this.primary.matches(otherInput.primary) && this.secondary.matches(otherInput.secondary); + } + + return false; + } +} + +// Register SideBySide/DiffEditor Input Serializer +interface ISerializedSideBySideEditorInput { + name: string; + description: string | undefined; + + primarySerialized: string; + secondarySerialized: string; + + primaryTypeId: string; + secondaryTypeId: string; +} + +export abstract class AbstractSideBySideEditorInputSerializer implements IEditorInputSerializer { + + private getInputSerializers(secondaryEditorInputTypeId: string, primaryEditorInputTypeId: string): [IEditorInputSerializer | undefined, IEditorInputSerializer | undefined] { + const registry = Registry.as(EditorExtensions.EditorInputFactories); + + return [registry.getEditorInputSerializer(secondaryEditorInputTypeId), registry.getEditorInputSerializer(primaryEditorInputTypeId)]; + } + + canSerialize(editorInput: EditorInput): boolean { + const input = editorInput as SideBySideEditorInput | DiffEditorInput; + + if (input.primary && input.secondary) { + const [secondaryInputSerializer, primaryInputSerializer] = this.getInputSerializers(input.secondary.typeId, input.primary.typeId); + + return !!(secondaryInputSerializer?.canSerialize(input.secondary) && primaryInputSerializer?.canSerialize(input.primary)); + } + + return false; + } + + serialize(editorInput: EditorInput): string | undefined { + const input = editorInput as SideBySideEditorInput; + + if (input.primary && input.secondary) { + const [secondaryInputSerializer, primaryInputSerializer] = this.getInputSerializers(input.secondary.typeId, input.primary.typeId); + if (primaryInputSerializer && secondaryInputSerializer) { + const primarySerialized = primaryInputSerializer.serialize(input.primary); + const secondarySerialized = secondaryInputSerializer.serialize(input.secondary); + + if (primarySerialized && secondarySerialized) { + const serializedEditorInput: ISerializedSideBySideEditorInput = { + name: input.getName(), + description: input.getDescription(), + primarySerialized: primarySerialized, + secondarySerialized: secondarySerialized, + primaryTypeId: input.primary.typeId, + secondaryTypeId: input.secondary.typeId + }; + + return JSON.stringify(serializedEditorInput); + } + } + } + + return undefined; + } + + deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput | undefined { + const deserialized: ISerializedSideBySideEditorInput = JSON.parse(serializedEditorInput); + + const [secondaryInputSerializer, primaryInputSerializer] = this.getInputSerializers(deserialized.secondaryTypeId, deserialized.primaryTypeId); + if (primaryInputSerializer && secondaryInputSerializer) { + const primaryInput = primaryInputSerializer.deserialize(instantiationService, deserialized.primarySerialized); + const secondaryInput = secondaryInputSerializer.deserialize(instantiationService, deserialized.secondarySerialized); + + if (primaryInput instanceof EditorInput && secondaryInput instanceof EditorInput) { + return this.createEditorInput(instantiationService, deserialized.name, deserialized.description, secondaryInput, primaryInput); + } + } + + return undefined; + } + + protected abstract createEditorInput(instantiationService: IInstantiationService, name: string, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput; +} + +export class SideBySideEditorInputSerializer extends AbstractSideBySideEditorInputSerializer { + + protected createEditorInput(instantiationService: IInstantiationService, name: string, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput { + return new SideBySideEditorInput(name, description, secondaryInput, primaryInput); + } +} diff --git a/src/vs/workbench/common/editor/textEditorModel.ts b/src/vs/workbench/common/editor/textEditorModel.ts index 49fa32a182..ae3f32b46b 100644 --- a/src/vs/workbench/common/editor/textEditorModel.ts +++ b/src/vs/workbench/common/editor/textEditorModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ITextModel, ITextBufferFactory, ITextSnapshot, ModelConstants } from 'vs/editor/common/model'; -import { EditorModel } from 'vs/workbench/common/editor'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { IModeSupport } from 'vs/workbench/services/textfile/common/textfiles'; import { URI } from 'vs/base/common/uri'; import { ITextEditorModel, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; diff --git a/src/vs/workbench/common/editor/textResourceEditorInput.ts b/src/vs/workbench/common/editor/textResourceEditorInput.ts index b72969a119..9b1206f92c 100644 --- a/src/vs/workbench/common/editor/textResourceEditorInput.ts +++ b/src/vs/workbench/common/editor/textResourceEditorInput.ts @@ -3,191 +3,35 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorInput, Verbosity, GroupIdentifier, IEditorInput, IRevertOptions, IEditorInputWithPreferredResource } from 'vs/workbench/common/editor'; +import { GroupIdentifier, IEditorInput, IRevertOptions, isTextEditorPane } from 'vs/workbench/common/editor'; +import { AbstractResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { URI } from 'vs/base/common/uri'; -import { ITextFileService, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, ITextFileSaveOptions, IModeSupport } from 'vs/workbench/services/textfile/common/textfiles'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; +import { IFileService } from 'vs/platform/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; -import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { Schemas } from 'vs/base/common/network'; -import { dirname, isEqual } from 'vs/base/common/resources'; +import { isEqual } from 'vs/base/common/resources'; +import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { TextResourceEditorModel } from 'vs/workbench/common/editor/textResourceEditorModel'; +import { IReference } from 'vs/base/common/lifecycle'; +import { IEditorViewState } from 'vs/editor/common/editorCommon'; +import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; /** * The base class for all editor inputs that open in text editors. */ -export abstract class AbstractTextResourceEditorInput extends EditorInput implements IEditorInputWithPreferredResource { - - private _preferredResource: URI; - get preferredResource(): URI { return this._preferredResource; } +export abstract class AbstractTextResourceEditorInput extends AbstractResourceEditorInput { constructor( - public readonly resource: URI, + resource: URI, preferredResource: URI | undefined, @IEditorService protected readonly editorService: IEditorService, - @IEditorGroupsService protected readonly editorGroupService: IEditorGroupsService, @ITextFileService protected readonly textFileService: ITextFileService, - @ILabelService protected readonly labelService: ILabelService, - @IFileService protected readonly fileService: IFileService, - @IFilesConfigurationService protected readonly filesConfigurationService: IFilesConfigurationService + @ILabelService labelService: ILabelService, + @IFileService fileService: IFileService ) { - super(); - - this._preferredResource = preferredResource || resource; - - this.registerListeners(); - } - - protected registerListeners(): void { - - // Clear label memoizer on certain events that have impact - this._register(this.labelService.onDidChangeFormatters(e => this.onLabelEvent(e.scheme))); - this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onLabelEvent(e.scheme))); - this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onLabelEvent(e.scheme))); - } - - private onLabelEvent(scheme: string): void { - if (scheme === this._preferredResource.scheme) { - this.updateLabel(); - } - } - - private updateLabel(): void { - - // Clear any cached labels from before - this._name = undefined; - this._shortDescription = undefined; - this._mediumDescription = undefined; - this._longDescription = undefined; - this._shortTitle = undefined; - this._mediumTitle = undefined; - this._longTitle = undefined; - - // Trigger recompute of label - this._onDidChangeLabel.fire(); - } - - setPreferredResource(preferredResource: URI): void { - if (!isEqual(preferredResource, this._preferredResource)) { - this._preferredResource = preferredResource; - - this.updateLabel(); - } - } - - private _name: string | undefined = undefined; - override getName(): string { - if (typeof this._name !== 'string') { - this._name = this.labelService.getUriBasenameLabel(this._preferredResource); - } - - return this._name; - } - - override getDescription(verbosity: Verbosity = Verbosity.MEDIUM): string | undefined { - switch (verbosity) { - case Verbosity.SHORT: - return this.shortDescription; - case Verbosity.LONG: - return this.longDescription; - case Verbosity.MEDIUM: - default: - return this.mediumDescription; - } - } - - private _shortDescription: string | undefined = undefined; - private get shortDescription(): string { - if (typeof this._shortDescription !== 'string') { - this._shortDescription = this.labelService.getUriBasenameLabel(dirname(this._preferredResource)); - } - - return this._shortDescription; - } - - private _mediumDescription: string | undefined = undefined; - private get mediumDescription(): string { - if (typeof this._mediumDescription !== 'string') { - this._mediumDescription = this.labelService.getUriLabel(dirname(this._preferredResource), { relative: true }); - } - - return this._mediumDescription; - } - - private _longDescription: string | undefined = undefined; - private get longDescription(): string { - if (typeof this._longDescription !== 'string') { - this._longDescription = this.labelService.getUriLabel(dirname(this._preferredResource)); - } - - return this._longDescription; - } - - private _shortTitle: string | undefined = undefined; - private get shortTitle(): string { - if (typeof this._shortTitle !== 'string') { - this._shortTitle = this.getName(); - } - - return this._shortTitle; - } - - private _mediumTitle: string | undefined = undefined; - private get mediumTitle(): string { - if (typeof this._mediumTitle !== 'string') { - this._mediumTitle = this.labelService.getUriLabel(this._preferredResource, { relative: true }); - } - - return this._mediumTitle; - } - - private _longTitle: string | undefined = undefined; - private get longTitle(): string { - if (typeof this._longTitle !== 'string') { - this._longTitle = this.labelService.getUriLabel(this._preferredResource); - } - - return this._longTitle; - } - - override getTitle(verbosity: Verbosity): string { - switch (verbosity) { - case Verbosity.SHORT: - return this.shortTitle; - case Verbosity.LONG: - return this.longTitle; - default: - case Verbosity.MEDIUM: - return this.mediumTitle; - } - } - - override isUntitled(): boolean { - // any file: is never untitled as it can be saved - // untitled: is untitled by definition - // any other: is untitled because it cannot be saved, as such we expect a "Save As" dialog - return !this.fileService.canHandleResource(this.resource); - } - - override isReadonly(): boolean { - if (this.isUntitled()) { - return false; // untitled is never readonly - } - - return this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); - } - - override isSaving(): boolean { - if (this.isUntitled()) { - return false; // untitled is never saving automatically - } - - if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) { - return true; // a short auto save is configured, treat this as being saved - } - - return false; + super(resource, preferredResource, labelService, fileService); } override save(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { @@ -236,4 +80,147 @@ export abstract class AbstractTextResourceEditorInput extends EditorInput implem override async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { await this.textFileService.revert(this.resource, options); } + + protected getViewStateFor(group: GroupIdentifier): IEditorViewState | undefined { + for (const editorPane of this.editorService.visibleEditorPanes) { + if (editorPane.group.id === group && this.matches(editorPane.input)) { + if (isTextEditorPane(editorPane)) { + return editorPane.getViewState(); + } + } + } + + return undefined; + } +} + +/** + * A read-only text editor input whos contents are made of the provided resource that points to an existing + * code editor model. + */ +export class TextResourceEditorInput extends AbstractTextResourceEditorInput implements IModeSupport { + + static readonly ID: string = 'workbench.editors.resourceEditorInput'; + + override get typeId(): string { + return TextResourceEditorInput.ID; + } + + private cachedModel: TextResourceEditorModel | undefined = undefined; + private modelReference: Promise> | undefined = undefined; + + constructor( + resource: URI, + private name: string | undefined, + private description: string | undefined, + private preferredMode: string | undefined, + private preferredContents: string | undefined, + @ITextModelService private readonly textModelResolverService: ITextModelService, + @ITextFileService textFileService: ITextFileService, + @IEditorService editorService: IEditorService, + @IFileService fileService: IFileService, + @ILabelService labelService: ILabelService + ) { + super(resource, undefined, editorService, textFileService, labelService, fileService); + } + + override getName(): string { + return this.name || super.getName(); + } + + setName(name: string): void { + if (this.name !== name) { + this.name = name; + + this._onDidChangeLabel.fire(); + } + } + + override getDescription(): string | undefined { + return this.description; + } + + setDescription(description: string): void { + if (this.description !== description) { + this.description = description; + + this._onDidChangeLabel.fire(); + } + } + + setMode(mode: string): void { + this.setPreferredMode(mode); + + if (this.cachedModel) { + this.cachedModel.setMode(mode); + } + } + + setPreferredMode(mode: string): void { + this.preferredMode = mode; + } + + setPreferredContents(contents: string): void { + this.preferredContents = contents; + } + + override async resolve(): Promise { + + // Unset preferred contents and mode after resolving + // once to prevent these properties to stick. We still + // want the user to change the language mode in the editor + // and want to show updated contents (if any) in future + // `resolve` calls. + const preferredContents = this.preferredContents; + const preferredMode = this.preferredMode; + this.preferredContents = undefined; + this.preferredMode = undefined; + + if (!this.modelReference) { + this.modelReference = this.textModelResolverService.createModelReference(this.resource); + } + + const ref = await this.modelReference; + + // Ensure the resolved model is of expected type + const model = ref.object; + if (!(model instanceof TextResourceEditorModel)) { + ref.dispose(); + this.modelReference = undefined; + + throw new Error(`Unexpected model for TextResourceEditorInput: ${this.resource}`); + } + + this.cachedModel = model; + + // Set contents and mode if preferred + if (typeof preferredContents === 'string' || typeof preferredMode === 'string') { + model.updateTextEditorModel(typeof preferredContents === 'string' ? createTextBufferFactory(preferredContents) : undefined, preferredMode); + } + + return model; + } + + override matches(otherInput: unknown): boolean { + if (super.matches(otherInput)) { + return true; + } + + if (otherInput instanceof TextResourceEditorInput) { + return isEqual(otherInput.resource, this.resource); + } + + return false; + } + + override dispose(): void { + if (this.modelReference) { + this.modelReference.then(ref => ref.dispose()); + this.modelReference = undefined; + } + + this.cachedModel = undefined; + + super.dispose(); + } } diff --git a/src/vs/workbench/common/editor/resourceEditorModel.ts b/src/vs/workbench/common/editor/textResourceEditorModel.ts similarity index 85% rename from src/vs/workbench/common/editor/resourceEditorModel.ts rename to src/vs/workbench/common/editor/textResourceEditorModel.ts index 93377f6026..bf37f0680d 100644 --- a/src/vs/workbench/common/editor/resourceEditorModel.ts +++ b/src/vs/workbench/common/editor/textResourceEditorModel.ts @@ -9,9 +9,10 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; /** - * An editor model for in-memory, readonly content that is backed by an existing editor model. + * An editor model for in-memory, readonly text content that + * is backed by an existing editor model. */ -export class ResourceEditorModel extends BaseTextEditorModel { +export class TextResourceEditorModel extends BaseTextEditorModel { constructor( resource: URI, diff --git a/src/vs/workbench/common/notifications.ts b/src/vs/workbench/common/notifications.ts index 7b74d4d174..9456ddcb25 100644 --- a/src/vs/workbench/common/notifications.ts +++ b/src/vs/workbench/common/notifications.ts @@ -619,13 +619,17 @@ export class NotificationViewItem extends Disposable implements INotificationVie } updateSeverity(severity: Severity): void { + if (severity === this._severity) { + return; + } + this._severity = severity; this._onDidChangeContent.fire({ kind: NotificationViewItemContentChangeKind.SEVERITY }); } updateMessage(input: NotificationMessage): void { const message = NotificationViewItem.parseNotificationMessage(input); - if (!message) { + if (!message || message.raw === this._message.raw) { return; } diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 8a5ab88f3d..07b267f341 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { registerColor, editorBackground, contrastBorder, transparent, editorWidgetBackground, textLinkForeground, lighten, darken, focusBorder, activeContrastBorder, editorWidgetForeground, editorErrorForeground, editorWarningForeground, editorInfoForeground, treeIndentGuidesStroke, errorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { registerColor, editorBackground, contrastBorder, transparent, editorWidgetBackground, textLinkForeground, lighten, darken, focusBorder, activeContrastBorder, editorWidgetForeground, editorErrorForeground, editorWarningForeground, editorInfoForeground, treeIndentGuidesStroke, errorForeground, listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme } from 'vs/platform/theme/common/themeService'; import { Color } from 'vs/base/common/color'; @@ -249,14 +249,6 @@ export const EDITOR_DRAG_AND_DROP_BACKGROUND = registerColor('editorGroup.dropBa hc: null }, localize('editorDragAndDropBackground', "Background color when dragging editors around. The color should have transparency so that the editor contents can still shine through.")); -// < --- Resource Viewer --- > - -export const IMAGE_PREVIEW_BORDER = registerColor('imagePreview.border', { - dark: Color.fromHex('#808080').transparent(0.35), - light: Color.fromHex('#808080').transparent(0.35), - hc: contrastBorder -}, localize('imagePreviewBorder', "Border color for image in image preview.")); - // < --- Panels --- > export const PANEL_BACKGROUND = registerColor('panel.background', { @@ -332,6 +324,25 @@ export const PANEL_SECTION_BORDER = registerColor('panelSection.border', { hc: PANEL_BORDER }, localize('panelSectionBorder', "Panel section border color used when multiple views are stacked horizontally in the panel. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); +// < --- Banner --- > + +export const BANNER_BACKGROUND = registerColor('banner.background', { + dark: listActiveSelectionBackground, + light: listActiveSelectionBackground, + hc: listActiveSelectionBackground +}, localize('banner.background', "Banner background color. The banner is shown under the title bar of the window.")); + +export const BANNER_FOREGROUND = registerColor('banner.foreground', { + dark: listActiveSelectionForeground, + light: listActiveSelectionForeground, + hc: listActiveSelectionForeground +}, localize('banner.foreground', "Banner foreground color. The banner is shown under the title bar of the window.")); + +export const BANNER_ICON_FOREGROUND = registerColor('banner.iconForeground', { + dark: editorInfoForeground, + light: editorInfoForeground, + hc: editorInfoForeground +}, localize('banner.iconForeground', "Banner icon color. The banner is shown under the title bar of the window.")); // < --- Status --- > diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts index d3c03204cf..08e74c511b 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts @@ -19,7 +19,7 @@ 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 { ILifecycleService, ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; class BulkEdit { @@ -195,7 +195,7 @@ export class BulkEditService implements IBulkEditService { let listener: IDisposable | undefined; try { - listener = this._lifecycleService.onBeforeShutdown(e => e.veto(this.shouldVeto(label), 'veto.blukEditService')); + listener = this._lifecycleService.onBeforeShutdown(e => e.veto(this.shouldVeto(label, e.reason), 'veto.blukEditService')); await bulkEdit.perform(); return { ariaSummary: bulkEdit.ariaMessage() }; } catch (err) { @@ -209,11 +209,13 @@ export class BulkEditService implements IBulkEditService { } } - private async shouldVeto(label: string | undefined): Promise { + private async shouldVeto(label: string | undefined, reason: ShutdownReason): Promise { label = label || localize('fileOperation', "File operation"); + const reasonLabel = reason === ShutdownReason.CLOSE ? localize('closeTheWindow', "Close Window") : reason === ShutdownReason.LOAD ? localize('changeWorkspace', "Change Workspace") : + reason === ShutdownReason.RELOAD ? localize('reloadTheWindow', "Reload Window") : localize('quit', "Quit"); 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") + message: localize('areYouSureQuiteBulkEdit', "Are you sure you want to {0}? '{1}' is in progress.", reasonLabel.toLowerCase(), label), + primaryButton: reasonLabel }); return !result.confirmed; diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts index b96511620e..030956ab64 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts @@ -102,7 +102,7 @@ class ModelEditTask implements IDisposable { class EditorEditTask extends ModelEditTask { - private _editor: ICodeEditor; + private readonly _editor: ICodeEditor; constructor(modelReference: IReference, editor: ICodeEditor) { super(modelReference); @@ -110,10 +110,18 @@ class EditorEditTask extends ModelEditTask { } override getBeforeCursorState(): Selection[] | null { - return this._editor.getSelections(); + return this._canUseEditor() ? this._editor.getSelections() : null; } override apply(): void { + + // Check that the editor is still for the wanted model. It might have changed in the + // meantime and that means we cannot use the editor anymore (instead we perform the edit through the model) + if (!this._canUseEditor()) { + super.apply(); + return; + } + if (this._edits.length > 0) { this._edits = this._edits.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); this._editor.executeEdits('', this._edits); @@ -124,6 +132,10 @@ class EditorEditTask extends ModelEditTask { } } } + + private _canUseEditor(): boolean { + return this._editor?.getModel()?.uri.toString() === this.model.uri.toString(); + } } export class BulkTextEdits { diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index f85918ec6b..62321cb9cc 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts @@ -245,7 +245,7 @@ export class BulkEditPane extends ViewPane { message = localize('conflict.N', "Cannot apply refactoring because {0} other files have changed in the meantime.", conflicts.length); } - this._dialogService.show(Severity.Warning, message, []).finally(() => this._done(false)); + this._dialogService.show(Severity.Warning, message).finally(() => this._done(false)); } discard() { @@ -353,8 +353,8 @@ export class BulkEditPane extends ViewPane { } this._editorService.openEditor({ - leftResource, - rightResource: previewUri, + originalInput: { resource: leftResource }, + modifiedInput: { resource: previewUri }, label, description: this._labelService.getUriLabel(dirname(leftResource), { relative: true }), options diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts index aec61b9a6b..89bd330b1f 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts @@ -297,6 +297,7 @@ export class CallHierarchyTreePeekWidget extends peekView.PeekViewWidget { // update: editor and editor highlights const options: IModelDecorationOptions = { + description: 'call-hierarchy-decoration', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'call-decoration', overviewRuler: { diff --git a/src/vs/workbench/contrib/cli/node/cli.contribution.ts b/src/vs/workbench/contrib/cli/node/cli.contribution.ts deleted file mode 100644 index 798d594e3d..0000000000 --- a/src/vs/workbench/contrib/cli/node/cli.contribution.ts +++ /dev/null @@ -1,193 +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 fs from 'fs'; -import * as cp from 'child_process'; -import * as nls from 'vs/nls'; -import * as path from 'vs/base/common/path'; -import * as pfs from 'vs/base/node/pfs'; -import * as extpath from 'vs/base/node/extpath'; -import { promisify } from 'util'; -import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; -import product from 'vs/platform/product/common/product'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import Severity from 'vs/base/common/severity'; -import { ILogService } from 'vs/platform/log/common/log'; -import { FileAccess } from 'vs/base/common/network'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IsMacNativeContext } from 'vs/platform/contextkey/common/contextkeys'; -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; - -function ignore(code: string, value: T): (err: any) => Promise { - return err => err.code === code ? Promise.resolve(value) : Promise.reject(err); -} - -let _source: string | null = null; -function getSource(): string { - if (!_source) { - const root = FileAccess.asFileUri('', require).fsPath; - _source = path.resolve(root, '..', 'bin', 'code'); - } - return _source; -} - -function isAvailable(): Promise { - return Promise.resolve(pfs.exists(getSource())); -} - -const category = nls.localize('shellCommand', "Shell Command"); - -class InstallAction extends Action2 { - - constructor() { - super({ - id: 'workbench.action.installCommandLine', - title: { - value: nls.localize('install', "Install '{0}' command in PATH", product.applicationName), - original: `Shell Command: Install \'${product.applicationName}\' command in PATH` - }, - category, - f1: true, - precondition: ContextKeyExpr.and(IsMacNativeContext, ContextKeyExpr.equals('remoteName', '')) - }); - } - - run(accessor: ServicesAccessor): Promise { - const productService = accessor.get(IProductService); - const notificationService = accessor.get(INotificationService); - const logService = accessor.get(ILogService); - const dialogService = accessor.get(IDialogService); - const target = `/usr/local/bin/${productService.applicationName}`; - - return isAvailable().then(isAvailable => { - if (!isAvailable) { - const message = nls.localize('not available', "This command is not available"); - notificationService.info(message); - return undefined; - } - - return this.isInstalled(target) - .then(isInstalled => { - if (!isAvailable || isInstalled) { - return Promise.resolve(null); - } else { - return fs.promises.unlink(target) - .then(undefined, ignore('ENOENT', null)) - .then(() => fs.promises.symlink(getSource(), target)) - .then(undefined, err => { - if (err.code === 'EACCES' || err.code === 'ENOENT') { - return new Promise((resolve, reject) => { - const buttons = [nls.localize('ok', "OK"), nls.localize('cancel2', "Cancel")]; - - dialogService.show(Severity.Info, nls.localize('warnEscalation', "Code will now prompt with 'osascript' for Administrator privileges to install the shell command."), buttons, { cancelId: 1 }).then(result => { - switch (result.choice) { - case 0 /* OK */: - const command = 'osascript -e "do shell script \\"mkdir -p /usr/local/bin && ln -sf \'' + getSource() + '\' \'' + target + '\'\\" with administrator privileges"'; - - promisify(cp.exec)(command, {}) - .then(undefined, _ => Promise.reject(new Error(nls.localize('cantCreateBinFolder', "Unable to create '/usr/local/bin'.")))) - .then(() => resolve(), reject); - break; - case 1 /* Cancel */: - reject(new Error(nls.localize('aborted', "Aborted"))); - break; - } - }); - }); - } - - return Promise.reject(err); - }); - } - }) - .then(() => { - logService.trace('cli#install', target); - notificationService.info(nls.localize('successIn', "Shell command '{0}' successfully installed in PATH.", productService.applicationName)); - }); - }); - } - - private async isInstalled(target: string): Promise { - try { - const stat = await fs.promises.lstat(target); - return stat.isSymbolicLink() && getSource() === await extpath.realpath(target); - } catch (err) { - if (err.code === 'ENOENT') { - return false; - } - - throw err; - } - } -} - -class UninstallAction extends Action2 { - - constructor() { - super({ - id: 'workbench.action.uninstallCommandLine', - title: { - value: nls.localize('uninstall', "Uninstall '{0}' command from PATH", product.applicationName), - original: `Shell Command: Uninstall \'${product.applicationName}\' command from PATH` - }, - category, - f1: true, - precondition: ContextKeyExpr.and(IsMacNativeContext, ContextKeyExpr.equals('remoteName', '')) - }); - } - - run(accessor: ServicesAccessor): Promise { - const productService = accessor.get(IProductService); - const notificationService = accessor.get(INotificationService); - const logService = accessor.get(ILogService); - const dialogService = accessor.get(IDialogService); - const target = `/usr/local/bin/${productService.applicationName}`; - - return isAvailable().then(isAvailable => { - if (!isAvailable) { - const message = nls.localize('not available', "This command is not available"); - notificationService.info(message); - return undefined; - } - - const uninstall = () => { - return fs.promises.unlink(target) - .then(undefined, ignore('ENOENT', null)); - }; - - return uninstall().then(undefined, err => { - if (err.code === 'EACCES') { - return new Promise(async (resolve, reject) => { - const buttons = [nls.localize('ok', "OK"), nls.localize('cancel2', "Cancel")]; - - const { choice } = await dialogService.show(Severity.Info, nls.localize('warnEscalationUninstall', "Code will now prompt with 'osascript' for Administrator privileges to uninstall the shell command."), buttons, { cancelId: 1 }); - switch (choice) { - case 0 /* OK */: - const command = 'osascript -e "do shell script \\"rm \'' + target + '\'\\" with administrator privileges"'; - - promisify(cp.exec)(command, {}) - .then(undefined, _ => Promise.reject(new Error(nls.localize('cantUninstall', "Unable to uninstall the shell command '{0}'.", target)))) - .then(() => resolve(), reject); - break; - case 1 /* Cancel */: - reject(new Error(nls.localize('aborted', "Aborted"))); - break; - } - }); - } - - return Promise.reject(err); - }).then(() => { - logService.trace('cli#uninstall', target); - notificationService.info(nls.localize('successFrom', "Shell command '{0}' successfully uninstalled from PATH.", productService.applicationName)); - }); - }); - } -} - -registerAction2(InstallAction); -registerAction2(UninstallAction); diff --git a/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts b/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts index 7cea122505..221daff506 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts @@ -18,4 +18,5 @@ import './toggleMultiCursorModifier'; import './toggleRenderControlCharacter'; import './toggleRenderWhitespace'; import './toggleWordWrap'; +import './untitledTextEditorHint'; import './workbenchReferenceSearch'; diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts index c219d7839b..6dc5741267 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts @@ -21,7 +21,7 @@ const enum WidgetState { class DiffEditorHelperContribution extends Disposable implements IDiffEditorContribution { - public static ID = 'editor.contrib.diffEditorHelper'; + public static readonly ID = 'editor.contrib.diffEditorHelper'; private _helperWidget: FloatingClickWidget | null; private _helperWidgetListener: IDisposable | null; diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts index 6b6247801e..a81a41b434 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts @@ -69,7 +69,7 @@ export abstract class SimpleFindWidget extends Widget { this._updateHistoryDelayer = new Delayer(500); this.oninput(this._findInput.domNode, (e) => { - this.foundMatch = this.onInputChanged(); + this.foundMatch = this._onInputChanged(); this.updateButtons(this.foundMatch); this._delayedUpdateHistory(); }); @@ -138,25 +138,25 @@ export abstract class SimpleFindWidget extends Widget { }); this._focusTracker = this._register(dom.trackFocus(this._innerDomNode)); - this._register(this._focusTracker.onDidFocus(this.onFocusTrackerFocus.bind(this))); - this._register(this._focusTracker.onDidBlur(this.onFocusTrackerBlur.bind(this))); + this._register(this._focusTracker.onDidFocus(this._onFocusTrackerFocus.bind(this))); + this._register(this._focusTracker.onDidBlur(this._onFocusTrackerBlur.bind(this))); this._findInputFocusTracker = this._register(dom.trackFocus(this._findInput.domNode)); - this._register(this._findInputFocusTracker.onDidFocus(this.onFindInputFocusTrackerFocus.bind(this))); - this._register(this._findInputFocusTracker.onDidBlur(this.onFindInputFocusTrackerBlur.bind(this))); + this._register(this._findInputFocusTracker.onDidFocus(this._onFindInputFocusTrackerFocus.bind(this))); + this._register(this._findInputFocusTracker.onDidBlur(this._onFindInputFocusTrackerBlur.bind(this))); this._register(dom.addDisposableListener(this._innerDomNode, 'click', (event) => { event.stopPropagation(); })); } - protected abstract onInputChanged(): boolean; + protected abstract _onInputChanged(): boolean; protected abstract find(previous: boolean): void; protected abstract findFirst(): void; - protected abstract onFocusTrackerFocus(): void; - protected abstract onFocusTrackerBlur(): void; - protected abstract onFindInputFocusTrackerFocus(): void; - protected abstract onFindInputFocusTrackerBlur(): void; + protected abstract _onFocusTrackerFocus(): void; + protected abstract _onFocusTrackerBlur(): void; + protected abstract _onFindInputFocusTrackerFocus(): void; + protected abstract _onFindInputFocusTrackerBlur(): void; protected get inputValue() { return this._findInput.getValue(); diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts index 30bca07ea4..ad69748bb9 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts @@ -416,10 +416,17 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { const fontStyleLabels = new Array(); function addStyle(key: 'bold' | 'italic' | 'underline') { + let label: HTMLElement | string | undefined; if (semantic && semantic[key]) { - fontStyleLabels.push($('span.tiw-metadata-semantic', undefined, key)); + label = $('span.tiw-metadata-semantic', undefined, key); } else if (tm && tm[key]) { - fontStyleLabels.push(key); + label = key; + } + if (label) { + if (fontStyleLabels.length) { + fontStyleLabels.push(' '); + } + fontStyleLabels.push(label); } } addStyle('bold'); @@ -428,7 +435,7 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { if (fontStyleLabels.length) { elements.push($('tr', undefined, $('td.tiw-metadata-key', undefined, 'font style' as string), - $('td.tiw-metadata-value', undefined, fontStyleLabels.join(' ')) + $('td.tiw-metadata-value', undefined, ...fontStyleLabels) )); } return elements; diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectKeybindings.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectKeybindings.ts index bc311848a8..903cae3f36 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectKeybindings.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectKeybindings.ts @@ -3,28 +3,26 @@ * 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 { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { CATEGORIES, Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { Action } from 'vs/base/common/actions'; +import { CATEGORIES } from 'vs/workbench/common/actions'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; class InspectKeyMap extends EditorAction { constructor() { super({ id: 'workbench.action.inspectKeyMappings', - label: nls.localize('workbench.action.inspectKeyMap', "Developer: Inspect Key Mappings"), + label: localize('workbench.action.inspectKeyMap', "Developer: Inspect Key Mappings"), alias: 'Developer: Inspect Key Mappings', precondition: undefined }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + run(accessor: ServicesAccessor, editor: ICodeEditor): void { const keybindingService = accessor.get(IKeybindingService); const editorService = accessor.get(IEditorService); @@ -34,23 +32,23 @@ class InspectKeyMap extends EditorAction { registerEditorAction(InspectKeyMap); -class InspectKeyMapJSON extends Action { - public static readonly ID = 'workbench.action.inspectKeyMappingsJSON'; - public static readonly LABEL = nls.localize('workbench.action.inspectKeyMapJSON', "Inspect Key Mappings (JSON)"); +class InspectKeyMapJSON extends Action2 { - constructor( - id: string, - label: string, - @IKeybindingService private readonly _keybindingService: IKeybindingService, - @IEditorService private readonly _editorService: IEditorService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.inspectKeyMappingsJSON', + title: { value: localize('workbench.action.inspectKeyMapJSON', "Inspect Key Mappings (JSON)"), original: 'Inspect Key Mappings (JSON)' }, + category: CATEGORIES.Developer, + f1: true + }); } - public override run(): Promise { - return this._editorService.openEditor({ contents: this._keybindingService._dumpDebugInfoJSON(), options: { pinned: true } }); + override async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const keybindingService = accessor.get(IKeybindingService); + + await editorService.openEditor({ contents: keybindingService._dumpDebugInfoJSON(), options: { pinned: true } }); } } -const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.from(InspectKeyMapJSON), 'Developer: Inspect Key Mappings (JSON)', CATEGORIES.Developer.value); +registerAction2(InspectKeyMapJSON); diff --git a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts index 5de584e7e8..caee9d5f05 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts @@ -233,6 +233,7 @@ class DocumentSymbolsOutline implements IOutline { const ids = this._editor.deltaDecorations([], [{ range: symbol.range, options: { + description: 'document-symbols-outline-range-highlight', className: 'rangeHighlight', isWholeLine: true } diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts index 1f30b8be2b..f9fcbd8407 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts @@ -17,6 +17,7 @@ import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; 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 { ITextEditorOptions } from 'vs/platform/editor/common/editor'; export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProvider { @@ -47,11 +48,13 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv 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, { + const editorOptions: ITextEditorOptions = { selection: options.range, pinned: options.keyMods.ctrlCmd || this.configuration.openEditorPinned, preserveFocus: options.preserveFocus - }, SIDE_GROUP); + }; + + this.editorService.openEditor(this.editorService.activeEditor, editorOptions, SIDE_GROUP); } // Otherwise let parent handle it diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index d424c69f53..d290819584 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -28,6 +28,7 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis 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'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccessProvider { @@ -73,11 +74,13 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess 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, { + const editorOptions: ITextEditorOptions = { selection: options.range, pinned: options.keyMods.ctrlCmd || this.configuration.openEditorPinned, preserveFocus: options.preserveFocus - }, SIDE_GROUP); + }; + + this.editorService.openEditor(this.editorService.activeEditor, editorOptions, SIDE_GROUP); } // Otherwise let parent handle it diff --git a/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts index d391575b2b..364e8f66e8 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts @@ -156,12 +156,12 @@ export class TrimFinalNewLinesParticipant implements ITextFileSaveParticipant { } /** - * returns 0 if the entire file is empty or whitespace only + * returns 0 if the entire file is empty */ - private findLastLineWithContent(model: ITextModel): number { + private findLastNonEmptyLine(model: ITextModel): number { for (let lineNumber = model.getLineCount(); lineNumber >= 1; lineNumber--) { const lineContent = model.getLineContent(lineNumber); - if (strings.lastNonWhitespaceIndex(lineContent) !== -1) { + if (lineContent.length > 0) { // this line has content return lineNumber; } @@ -193,8 +193,8 @@ export class TrimFinalNewLinesParticipant implements ITextFileSaveParticipant { } } - const lastLineNumberWithContent = this.findLastLineWithContent(model); - const deleteFromLineNumber = Math.max(lastLineNumberWithContent + 1, cannotTouchLineNumber + 1); + const lastNonEmptyLine = this.findLastNonEmptyLine(model); + const deleteFromLineNumber = Math.max(lastNonEmptyLine + 1, cannotTouchLineNumber + 1); const deletionRange = model.validateRange(new Range(deleteFromLineNumber, 1, lineCount, model.getLineMaxColumn(lineCount))); if (deletionRange.isEmpty()) { @@ -233,9 +233,10 @@ class FormatOnSaveParticipant implements ITextFileSaveParticipant { const nestedProgress = new Progress<{ displayName?: string, extensionId?: ExtensionIdentifier }>(provider => { progress.report({ message: localize( - 'formatting', - "Running '{0}' Formatter ([configure](command:workbench.action.openSettings?%5B%22editor.formatOnSave%22%5D)).", - provider.displayName || provider.extensionId && provider.extensionId.value || '???' + { key: 'formatting2', comment: ['[configure]({1}) is a link. Only translate `configure`. Do not change brackets and parentheses or {1}'] }, + "Running '{0}' Formatter ([configure]({1})).", + provider.displayName || provider.extensionId && provider.extensionId.value || '???', + 'command:workbench.action.openSettings?%5B%22editor.formatOnSave%22%5D' ) }); }); @@ -336,9 +337,10 @@ class CodeActionOnSaveParticipant implements ITextFileSaveParticipant { private _report(): void { progress.report({ message: localize( - 'codeaction.get', - "Getting code actions from '{0}' ([configure](command:workbench.action.openSettings?%5B%22editor.codeActionsOnSave%22%5D)).", - [...this._names].map(name => `'${name}'`).join(', ') + { key: 'codeaction.get2', comment: ['[configure]({1}) is a link. Only translate `configure`. Do not change brackets and parentheses or {1}'] }, + "Getting code actions from '{0}' ([configure]({1})).", + [...this._names].map(name => `'${name}'`).join(', '), + 'command:workbench.action.openSettings?%5B%22editor.codeActionsOnSave%22%5D' ) }); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts index 74acadff20..4f30738f91 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts @@ -3,13 +3,10 @@ * 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 { MenuId, MenuRegistry, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -17,34 +14,39 @@ import { CoreNavigationCommands } from 'vs/editor/browser/controller/coreCommand import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { CursorColumns } from 'vs/editor/common/controller/cursorCommon'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -export class ToggleColumnSelectionAction extends Action { - public static readonly ID = 'editor.action.toggleColumnSelection'; - public static readonly LABEL = nls.localize('toggleColumnSelection', "Toggle Column Selection Mode"); +export class ToggleColumnSelectionAction extends Action2 { - constructor( - id: string, - label: string, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @ICodeEditorService private readonly _codeEditorService: ICodeEditorService - ) { - super(id, label); + static readonly ID = 'editor.action.toggleColumnSelection'; + + constructor() { + super({ + id: ToggleColumnSelectionAction.ID, + title: { + value: localize('toggleColumnSelection', "Toggle Column Selection Mode"), + mnemonicTitle: localize({ key: 'miColumnSelection', comment: ['&& denotes a mnemonic'] }, "Column &&Selection Mode"), + original: 'Toggle Column Selection Mode' + }, + f1: true, + toggled: ContextKeyExpr.equals('config.editor.columnSelection', true), + menu: { + id: MenuId.MenubarSelectionMenu, + group: '4_config', + order: 2 + } + }); } - private _getCodeEditor(): ICodeEditor | null { - const codeEditor = this._codeEditorService.getFocusedCodeEditor(); - if (codeEditor) { - return codeEditor; - } - return this._codeEditorService.getActiveCodeEditor(); - } + override async run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + const codeEditorService = accessor.get(ICodeEditorService); - public override async run(): Promise { - const oldValue = this._configurationService.getValue('editor.columnSelection'); - const codeEditor = this._getCodeEditor(); - await this._configurationService.updateValue('editor.columnSelection', !oldValue); - const newValue = this._configurationService.getValue('editor.columnSelection'); - if (!codeEditor || codeEditor !== this._getCodeEditor() || oldValue === newValue || !codeEditor.hasModel()) { + const oldValue = configurationService.getValue('editor.columnSelection'); + const codeEditor = this._getCodeEditor(codeEditorService); + await configurationService.updateValue('editor.columnSelection', !oldValue); + const newValue = configurationService.getValue('editor.columnSelection'); + if (!codeEditor || codeEditor !== this._getCodeEditor(codeEditorService) || oldValue === newValue || !codeEditor.hasModel()) { return; } const viewModel = codeEditor._getViewModel(); @@ -76,17 +78,14 @@ export class ToggleColumnSelectionAction extends Action { codeEditor.setSelection(new Selection(fromPosition.lineNumber, fromPosition.column, toPosition.lineNumber, toPosition.column)); } } + + private _getCodeEditor(codeEditorService: ICodeEditorService): ICodeEditor | null { + const codeEditor = codeEditorService.getFocusedCodeEditor(); + if (codeEditor) { + return codeEditor; + } + return codeEditorService.getActiveCodeEditor(); + } } -const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleColumnSelectionAction), 'Toggle Column Selection Mode'); - -MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { - group: '4_config', - command: { - id: ToggleColumnSelectionAction.ID, - title: nls.localize({ key: 'miColumnSelection', comment: ['&& denotes a mnemonic'] }, "Column &&Selection Mode"), - toggled: ContextKeyExpr.equals('config.editor.columnSelection', true) - }, - order: 2 -}); +registerAction2(ToggleColumnSelectionAction); diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleMinimap.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleMinimap.ts index 7aecbe92eb..b4c2ae70fd 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleMinimap.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleMinimap.ts @@ -3,43 +3,42 @@ * 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 { /*MenuId, MenuRegistry,*/ SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -// import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { CATEGORIES, Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { CATEGORIES } from 'vs/workbench/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -export class ToggleMinimapAction extends Action { - public static readonly ID = 'editor.action.toggleMinimap'; - public static readonly LABEL = nls.localize('toggleMinimap', "Toggle Minimap"); +export class ToggleMinimapAction extends Action2 { - constructor( - id: string, - label: string, - @IConfigurationService private readonly _configurationService: IConfigurationService - ) { - super(id, label); + static readonly ID = 'editor.action.toggleMinimap'; + + constructor() { + super({ + id: ToggleMinimapAction.ID, + title: { + value: localize('toggleMinimap', "Toggle Minimap"), + original: 'Toggle Minimap', + mnemonicTitle: localize({ key: 'miShowMinimap', comment: ['&& denotes a mnemonic'] }, "Show &&Minimap") + }, + category: CATEGORIES.View, + f1: true, + toggled: ContextKeyExpr.equals('config.editor.minimap.enabled', true), + menu: { + id: MenuId.MenubarViewMenu, + group: '5_editor', + order: 2 + } + }); } - public override run(): Promise { - const newValue = !this._configurationService.getValue('editor.minimap.enabled'); - return this._configurationService.updateValue('editor.minimap.enabled', newValue); + override async run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + + const newValue = !configurationService.getValue('editor.minimap.enabled'); + return configurationService.updateValue('editor.minimap.enabled', newValue); } } -const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleMinimapAction), 'View: Toggle Minimap', CATEGORIES.View.value); - -/* {{SQL CARBON EDIT}} - Disable unused menu item -MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '5_editor', - command: { - id: ToggleMinimapAction.ID, - title: nls.localize({ key: 'miShowMinimap', comment: ['&& denotes a mnemonic'] }, "Show &&Minimap"), - toggled: ContextKeyExpr.equals('config.editor.minimap.enabled', true) - }, - order: 2 -}); -*/ +registerAction2(ToggleMinimapAction); diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts index 51c8f8bdb7..f62b9a23f2 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts @@ -3,37 +3,37 @@ * 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 * as platform from 'vs/base/common/platform'; -import { MenuId, MenuRegistry, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; +import { isMacintosh } from 'vs/base/common/platform'; +import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -export class ToggleMultiCursorModifierAction extends Action { +export class ToggleMultiCursorModifierAction extends Action2 { - public static readonly ID = 'workbench.action.toggleMultiCursorModifier'; - public static readonly LABEL = nls.localize('toggleLocation', "Toggle Multi-Cursor Modifier"); + static readonly ID = 'workbench.action.toggleMultiCursorModifier'; private static readonly multiCursorModifierConfigurationKey = 'editor.multiCursorModifier'; - constructor( - id: string, - label: string, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super(id, label); + constructor() { + super({ + id: ToggleMultiCursorModifierAction.ID, + title: { value: localize('toggleLocation', "Toggle Multi-Cursor Modifier"), original: 'Toggle Multi-Cursor Modifier' }, + f1: true + }); } - public override run(): Promise { - const editorConf = this.configurationService.getValue<{ multiCursorModifier: 'ctrlCmd' | 'alt' }>('editor'); + override run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + + const editorConf = configurationService.getValue<{ multiCursorModifier: 'ctrlCmd' | 'alt' }>('editor'); const newValue: 'ctrlCmd' | 'alt' = (editorConf.multiCursorModifier === 'ctrlCmd' ? 'alt' : 'ctrlCmd'); - return this.configurationService.updateValue(ToggleMultiCursorModifierAction.multiCursorModifierConfigurationKey, newValue); + return configurationService.updateValue(ToggleMultiCursorModifierAction.multiCursorModifierConfigurationKey, newValue); } } @@ -66,14 +66,13 @@ class MultiCursorModifierContextKeyController implements IWorkbenchContribution Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(MultiCursorModifierContextKeyController, LifecyclePhase.Restored); +registerAction2(ToggleMultiCursorModifierAction); -const registry = Registry.as(Extensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleMultiCursorModifierAction), 'Toggle Multi-Cursor Modifier'); MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { group: '4_config', command: { id: ToggleMultiCursorModifierAction.ID, - title: nls.localize('miMultiCursorAlt', "Switch to Alt+Click for Multi-Cursor") + title: localize('miMultiCursorAlt', "Switch to Alt+Click for Multi-Cursor") }, when: multiCursorModifier.isEqualTo('ctrlCmd'), order: 1 @@ -83,9 +82,9 @@ MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { command: { id: ToggleMultiCursorModifierAction.ID, title: ( - platform.isMacintosh - ? nls.localize('miMultiCursorCmd', "Switch to Cmd+Click for Multi-Cursor") - : nls.localize('miMultiCursorCtrl', "Switch to Ctrl+Click for Multi-Cursor") + isMacintosh + ? localize('miMultiCursorCmd', "Switch to Cmd+Click for Multi-Cursor") + : localize('miMultiCursorCtrl', "Switch to Ctrl+Click for Multi-Cursor") ) }, when: multiCursorModifier.isEqualTo('altKey'), diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleRenderControlCharacter.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleRenderControlCharacter.ts index d1b41f5acf..1625ba81a9 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleRenderControlCharacter.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleRenderControlCharacter.ts @@ -3,44 +3,42 @@ * 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 { /*MenuId, MenuRegistry,*/ SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -// import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { CATEGORIES, Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { CATEGORIES, } from 'vs/workbench/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -export class ToggleRenderControlCharacterAction extends Action { +export class ToggleRenderControlCharacterAction extends Action2 { - public static readonly ID = 'editor.action.toggleRenderControlCharacter'; - public static readonly LABEL = nls.localize('toggleRenderControlCharacters', "Toggle Control Characters"); + static readonly ID = 'editor.action.toggleRenderControlCharacter'; - constructor( - id: string, - label: string, - @IConfigurationService private readonly _configurationService: IConfigurationService - ) { - super(id, label); + constructor() { + super({ + id: ToggleRenderControlCharacterAction.ID, + title: { + value: localize('toggleRenderControlCharacters', "Toggle Control Characters"), + mnemonicTitle: localize({ key: 'miToggleRenderControlCharacters', comment: ['&& denotes a mnemonic'] }, "Render &&Control Characters"), + original: 'Toggle Control Characters' + }, + category: CATEGORIES.View, + f1: true, + toggled: ContextKeyExpr.equals('config.editor.renderControlCharacters', true), + menu: { + id: MenuId.MenubarViewMenu, + group: '5_editor', + order: 5 + } + }); } - public override run(): Promise { - let newRenderControlCharacters = !this._configurationService.getValue('editor.renderControlCharacters'); - return this._configurationService.updateValue('editor.renderControlCharacters', newRenderControlCharacters); + override run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + + const newRenderControlCharacters = !configurationService.getValue('editor.renderControlCharacters'); + return configurationService.updateValue('editor.renderControlCharacters', newRenderControlCharacters); } } -const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleRenderControlCharacterAction), 'View: Toggle Control Characters', CATEGORIES.View.value); - -/* {{SQL CARBON EDIT}} - Disable unused menu item -MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '5_editor', - command: { - id: ToggleRenderControlCharacterAction.ID, - title: nls.localize({ key: 'miToggleRenderControlCharacters', comment: ['&& denotes a mnemonic'] }, "Render &&Control Characters"), - toggled: ContextKeyExpr.equals('config.editor.renderControlCharacters', true) - }, - order: 5 -}); -*/ +registerAction2(ToggleRenderControlCharacterAction); diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleRenderWhitespace.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleRenderWhitespace.ts index 820925ec58..30f5b0fd08 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleRenderWhitespace.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleRenderWhitespace.ts @@ -3,29 +3,40 @@ * 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 { /*MenuId, MenuRegistry,*/ SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -// import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { CATEGORIES, Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { CATEGORIES, } from 'vs/workbench/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -export class ToggleRenderWhitespaceAction extends Action { +class ToggleRenderWhitespaceAction extends Action2 { - public static readonly ID = 'editor.action.toggleRenderWhitespace'; - public static readonly LABEL = nls.localize('toggleRenderWhitespace', "Toggle Render Whitespace"); + static readonly ID = 'editor.action.toggleRenderWhitespace'; - constructor( - id: string, - label: string, - @IConfigurationService private readonly _configurationService: IConfigurationService - ) { - super(id, label); + constructor() { + super({ + id: ToggleRenderWhitespaceAction.ID, + title: { + value: localize('toggleRenderWhitespace', "Toggle Render Whitespace"), + mnemonicTitle: localize({ key: 'miToggleRenderWhitespace', comment: ['&& denotes a mnemonic'] }, "&&Render Whitespace"), + original: 'Toggle Render Whitespace' + }, + category: CATEGORIES.View, + f1: true, + toggled: ContextKeyExpr.notEquals('config.editor.renderWhitespace', 'none'), + menu: { + id: MenuId.MenubarViewMenu, + group: '5_editor', + order: 4 + } + }); } - public override run(): Promise { - const renderWhitespace = this._configurationService.getValue('editor.renderWhitespace'); + override run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + + const renderWhitespace = configurationService.getValue('editor.renderWhitespace'); let newRenderWhitespace: string; if (renderWhitespace === 'none') { @@ -34,21 +45,8 @@ export class ToggleRenderWhitespaceAction extends Action { newRenderWhitespace = 'none'; } - return this._configurationService.updateValue('editor.renderWhitespace', newRenderWhitespace); + return configurationService.updateValue('editor.renderWhitespace', newRenderWhitespace); } } -const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleRenderWhitespaceAction), 'View: Toggle Render Whitespace', CATEGORIES.View.value); - -/* {{SQL CARBON EDIT}} - Disable unused menu item -MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '5_editor', - command: { - id: ToggleRenderWhitespaceAction.ID, - title: nls.localize({ key: 'miToggleRenderWhitespace', comment: ['&& denotes a mnemonic'] }, "&&Render Whitespace"), - toggled: ContextKeyExpr.notEquals('config.editor.renderWhitespace', 'none') - }, - order: 4 -}); -*/ +registerAction2(ToggleRenderWhitespaceAction); diff --git a/src/vs/workbench/browser/parts/editor/untitledHint.ts b/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts similarity index 83% rename from src/vs/workbench/browser/parts/editor/untitledHint.ts rename to src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts index 04a3dc37ce..9c5529eea1 100644 --- a/src/vs/workbench/browser/parts/editor/untitledHint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; @@ -17,18 +17,19 @@ import { Schemas } from 'vs/base/common/network'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; + const $ = dom.$; -const untitledHintSetting = 'workbench.editor.untitled.hint'; -export class UntitledHintContribution implements IEditorContribution { +const untitledTextEditorHintSetting = 'workbench.editor.untitled.hint'; +export class UntitledTextEditorHintContribution implements IEditorContribution { - public static readonly ID = 'editor.contrib.untitledHint'; + public static readonly ID = 'editor.contrib.untitledTextEditorHint'; private toDispose: IDisposable[]; - private untitledHintContentWidget: UntitledHintContentWidget | undefined; + private untitledTextHintContentWidget: UntitledTextEditorHintContentWidget | undefined; private experimentTreatment: 'text' | 'hidden' | undefined; - constructor( private editor: ICodeEditor, @ICommandService private readonly commandService: ICommandService, @@ -40,7 +41,7 @@ export class UntitledHintContribution implements IEditorContribution { this.toDispose.push(this.editor.onDidChangeModel(() => this.update())); this.toDispose.push(this.editor.onDidChangeModelLanguage(() => this.update())); this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(untitledHintSetting)) { + if (e.affectsConfiguration(untitledTextEditorHintSetting)) { this.update(); } })); @@ -51,24 +52,24 @@ export class UntitledHintContribution implements IEditorContribution { } private update(): void { - this.untitledHintContentWidget?.dispose(); - const configValue = this.configurationService.getValue<'text' | 'hidden' | 'default'>(untitledHintSetting); + this.untitledTextHintContentWidget?.dispose(); + const configValue = this.configurationService.getValue<'text' | 'hidden' | 'default'>(untitledTextEditorHintSetting); const untitledHintMode = configValue === 'default' ? (this.experimentTreatment || 'text') : configValue; const model = this.editor.getModel(); if (model && model.uri.scheme === Schemas.untitled && model.getModeId() === PLAINTEXT_MODE_ID && untitledHintMode === 'text') { - this.untitledHintContentWidget = new UntitledHintContentWidget(this.editor, this.commandService, this.configurationService); + this.untitledTextHintContentWidget = new UntitledTextEditorHintContentWidget(this.editor, this.commandService, this.configurationService); } } dispose(): void { dispose(this.toDispose); - this.untitledHintContentWidget?.dispose(); + this.untitledTextHintContentWidget?.dispose(); } } -class UntitledHintContentWidget implements IContentWidget { +class UntitledTextEditorHintContentWidget implements IContentWidget { private static readonly ID = 'editor.widget.untitledHint'; @@ -99,7 +100,7 @@ class UntitledHintContentWidget implements IContentWidget { } getId(): string { - return UntitledHintContentWidget.ID; + return UntitledTextEditorHintContentWidget.ID; } // Select a language to get started. Start typing to dismiss, or don't show this again. @@ -109,7 +110,7 @@ class UntitledHintContentWidget implements IContentWidget { this.domNode.style.width = 'max-content'; const language = $('a.language-mode'); language.style.cursor = 'pointer'; - language.innerText = localize('selectAlanguage', "Select a language"); + language.innerText = localize('selectAlanguage2', "Select a language"); this.domNode.appendChild(language); const toGetStarted = $('span'); toGetStarted.innerText = localize('toGetStarted', " to get started. Start typing to dismiss, or ",); @@ -133,7 +134,7 @@ class UntitledHintContentWidget implements IContentWidget { })); this.toDispose.push(dom.addDisposableListener(dontShow, 'click', () => { - this.configurationService.updateValue(untitledHintSetting, 'hidden'); + this.configurationService.updateValue(untitledTextEditorHintSetting, 'hidden'); this.dispose(); this.editor.focus(); })); @@ -173,3 +174,5 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.monaco-editor .contentWidgets .untitled-hint a { color: ${textLinkForegroundColor}; }`); } }); + +registerEditorContribution(UntitledTextEditorHintContribution.ID, UntitledTextEditorHintContribution); diff --git a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts index 80625ed5d9..ec90676df8 100644 --- a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts @@ -29,6 +29,7 @@ export class CommentGlyphWidget { private createDecorationOptions(): ModelDecorationOptions { const decorationOptions: IModelDecorationOptions = { + description: 'comment-glyph-widget', isWholeLine: true, overviewRuler: { color: themeColorFromId(overviewRulerCommentingRangeForeground), diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index bf45dac8e2..cc105acbbc 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -30,7 +30,7 @@ import { IMenu, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/co import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { contrastBorder, editorForeground, focusBorder, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, transparent } from 'vs/platform/theme/common/colorRegistry'; +import { contrastBorder, editorForeground, focusBorder, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, resolveColorValue, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { CommentFormActions } from 'vs/workbench/contrib/comments/browser/commentFormActions'; import { CommentGlyphWidget } from 'vs/workbench/contrib/comments/browser/commentGlyphWidget'; @@ -805,12 +805,12 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget renderOptions: { after: { contentText: placeholder, - color: `${transparent(editorForeground, 0.4)(this.themeService.getColorTheme())}` + color: `${resolveColorValue(editorForeground, this.themeService.getColorTheme())?.transparent(0.4)}` } } }]; - this._commentReplyComponent?.editor.setDecorations(COMMENTEDITOR_DECORATION_KEY, decorations); + this._commentReplyComponent?.editor.setDecorations('review-zone-widget', COMMENTEDITOR_DECORATION_KEY, decorations); } } diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index 33f3446c48..1c7245040c 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -102,6 +102,7 @@ class CommentingRangeDecorator { constructor() { const decorationOptions: IModelDecorationOptions = { + description: 'commenting-range-decorator', isWholeLine: true, linesDecorationsClassName: 'comment-range-glyph comment-diff-added' }; @@ -196,7 +197,7 @@ export class CommentController implements IEditorContribution { })); this.globalToDispose.add(this.editor.onDidChangeModel(e => this.onModelChanged(e))); - this.codeEditorService.registerDecorationType(COMMENTEDITOR_DECORATION_KEY, {}); + this.codeEditorService.registerDecorationType('comment-controller', COMMENTEDITOR_DECORATION_KEY, {}); this.beginCompute(); } diff --git a/src/vs/workbench/contrib/comments/common/commentContextKeys.ts b/src/vs/workbench/contrib/comments/common/commentContextKeys.ts index ef1788c738..bec4eeb125 100644 --- a/src/vs/workbench/contrib/comments/common/commentContextKeys.ts +++ b/src/vs/workbench/contrib/comments/common/commentContextKeys.ts @@ -14,4 +14,4 @@ export namespace CommentContextKeys { * A context key that is set when the comment has no input. */ export const commentIsEmpty = new RawContextKey('commentIsEmpty', false); -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/comments/common/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/common/commentThreadWidget.ts index e5b025bd99..cda30b1f91 100644 --- a/src/vs/workbench/contrib/comments/common/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/common/commentThreadWidget.ts @@ -6,4 +6,4 @@ export interface ICommentThreadWidget { submitComment: () => Promise; collapse: () => void; -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditor.contribution.ts b/src/vs/workbench/contrib/customEditor/browser/customEditor.contribution.ts index e48d9a81f6..4e878e781a 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditor.contribution.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditor.contribution.ts @@ -3,15 +3,16 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Schemas } from 'vs/base/common/network'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorDescriptor, IEditorRegistry } from 'vs/workbench/browser/editor'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { EditorExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; -import { customEditorInputFactory, CustomEditorInputSerializer } from 'vs/workbench/contrib/customEditor/browser/customEditorInputFactory'; +import { CustomEditorInputSerializer, ComplexCustomWorkingCopyEditorHandler as ComplexCustomWorkingCopyEditorHandler } 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 { CustomEditorService } from './customEditors'; @@ -32,5 +33,5 @@ Registry.as(EditorExtensions.EditorInputFactories) CustomEditorInputSerializer.ID, CustomEditorInputSerializer); -Registry.as(EditorExtensions.EditorInputFactories) - .registerCustomEditorInputFactory(Schemas.vscodeCustomEditor, customEditorInputFactory); +Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(ComplexCustomWorkingCopyEditorHandler, LifecyclePhase.Starting); diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 83231e6d6b..308f9af632 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -4,22 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import { VSBuffer } from 'vs/base/common/buffer'; -import { memoize } from 'vs/base/common/decorators'; import { IReference } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { basename } from 'vs/base/common/path'; -import { isEqual } from 'vs/base/common/resources'; +import { dirname, isEqual } from 'vs/base/common/resources'; import { assertIsDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { GroupIdentifier, IEditorInput, IRevertOptions, ISaveOptions, Verbosity } from 'vs/workbench/common/editor'; +import { EditorInputCapabilities, GroupIdentifier, IEditorInput, IRevertOptions, ISaveOptions, Verbosity } from 'vs/workbench/common/editor'; +import { decorateFileEditorLabel } from 'vs/workbench/common/editor/resourceEditorInput'; import { defaultCustomEditor } from 'vs/workbench/contrib/customEditor/common/contributedCustomEditors'; import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; -import { decorateFileEditorLabel } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; 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'; @@ -32,7 +33,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { resource: URI, viewType: string, group: GroupIdentifier | undefined, - options?: { readonly customClasses?: string }, + options?: { readonly customClasses?: string, readonly oldResource?: URI }, ): IEditorInput { return instantiationService.invokeFunction(accessor => { if (viewType === defaultCustomEditor.id) { @@ -43,11 +44,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { let untitledDocumentData = untitledString ? VSBuffer.fromString(untitledString) : undefined; const id = generateUuid(); const webview = accessor.get(IWebviewService).createWebviewOverlay(id, { customClasses: options?.customClasses }, {}, undefined); - const input = instantiationService.createInstance(CustomEditorInput, resource, viewType, id, webview, { untitledDocumentData: untitledDocumentData }); - // If we're loading untitled file data we should ensure it's dirty - if (untitledDocumentData) { - input._defaultDirtyState = true; - } + const input = instantiationService.createInstance(CustomEditorInput, resource, viewType, id, webview, { untitledDocumentData: untitledDocumentData, oldResource: options?.oldResource }); if (typeof group !== 'undefined') { input.updateGroup(group); } @@ -58,6 +55,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { public static override readonly typeId = 'workbench.editors.webviewEditor'; private readonly _editorResource: URI; + public readonly oldResource?: URI; private _defaultDirtyState: boolean | undefined; private readonly _backupId: string | undefined; @@ -73,7 +71,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { viewType: string, id: string, webview: WebviewOverlay, - options: { startsDirty?: boolean, backupId?: string, untitledDocumentData?: VSBuffer }, + options: { startsDirty?: boolean, backupId?: string, untitledDocumentData?: VSBuffer, readonly oldResource?: URI }, @IWebviewWorkbenchService webviewWorkbenchService: IWebviewWorkbenchService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ILabelService private readonly labelService: ILabelService, @@ -81,20 +79,72 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { @IFileDialogService private readonly fileDialogService: IFileDialogService, @IEditorService private readonly editorService: IEditorService, @IUndoRedoService private readonly undoRedoService: IUndoRedoService, + @IFileService private readonly fileService: IFileService ) { super(id, viewType, '', webview, webviewWorkbenchService); this._editorResource = resource; + this.oldResource = options.oldResource; this._defaultDirtyState = options.startsDirty; this._backupId = options.backupId; this._untitledDocumentData = options.untitledDocumentData; + + this.registerListeners(); + } + + private registerListeners(): void { + + // Clear our labels on certain label related events + this._register(this.labelService.onDidChangeFormatters(e => this.onLabelEvent(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onLabelEvent(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onLabelEvent(e.scheme))); + } + + private onLabelEvent(scheme: string): void { + if (scheme === this.resource.scheme) { + this.updateLabel(); + } + } + + private updateLabel(): void { + + // Clear any cached labels from before + this._shortDescription = undefined; + this._mediumDescription = undefined; + this._longDescription = undefined; + this._shortTitle = undefined; + this._mediumTitle = undefined; + this._longTitle = undefined; + + // Trigger recompute of label + this._onDidChangeLabel.fire(); } public override get typeId(): string { return CustomEditorInput.typeId; } - public override canSplit() { - return !!this.customEditorService.getCustomEditorCapabilities(this.viewType)?.supportsMultipleEditorsPerDocument; + public override get capabilities(): EditorInputCapabilities { + let capabilities = EditorInputCapabilities.None; + + if (!this.customEditorService.getCustomEditorCapabilities(this.viewType)?.supportsMultipleEditorsPerDocument) { + capabilities |= EditorInputCapabilities.Singleton; + } + + if (this._modelRef) { + if (this._modelRef.object.isReadonly()) { + capabilities |= EditorInputCapabilities.Readonly; + } + } else { + if (this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly)) { + capabilities |= EditorInputCapabilities.Readonly; + } + } + + if (this.resource.scheme === Schemas.untitled) { + capabilities |= EditorInputCapabilities.Untitled; + } + + return capabilities; } override getName(): string { @@ -102,64 +152,101 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { return this.decorateLabel(name); } - override matches(other: IEditorInput): boolean { + override getDescription(verbosity = Verbosity.MEDIUM): string | undefined { + switch (verbosity) { + case Verbosity.SHORT: + return this.shortDescription; + case Verbosity.LONG: + return this.longDescription; + case Verbosity.MEDIUM: + default: + return this.mediumDescription; + } + } + + private _shortDescription: string | undefined = undefined; + private get shortDescription(): string { + if (typeof this._shortDescription !== 'string') { + this._shortDescription = this.labelService.getUriBasenameLabel(dirname(this.resource)); + } + + return this._shortDescription; + } + + private _mediumDescription: string | undefined = undefined; + private get mediumDescription(): string { + if (typeof this._mediumDescription !== 'string') { + this._mediumDescription = this.labelService.getUriLabel(dirname(this.resource), { relative: true }); + } + + return this._mediumDescription; + } + + private _longDescription: string | undefined = undefined; + private get longDescription(): string { + if (typeof this._longDescription !== 'string') { + this._longDescription = this.labelService.getUriLabel(dirname(this.resource)); + } + + return this._longDescription; + } + + private _shortTitle: string | undefined = undefined; + private get shortTitle(): string { + if (typeof this._shortTitle !== 'string') { + this._shortTitle = this.getName(); + } + + return this._shortTitle; + } + + private _mediumTitle: string | undefined = undefined; + private get mediumTitle(): string { + if (typeof this._mediumTitle !== 'string') { + this._mediumTitle = this.labelService.getUriLabel(this.resource, { relative: true }); + } + + return this._mediumTitle; + } + + private _longTitle: string | undefined = undefined; + private get longTitle(): string { + if (typeof this._longTitle !== 'string') { + this._longTitle = this.labelService.getUriLabel(this.resource); + } + + return this._longTitle; + } + + override getTitle(verbosity?: Verbosity): string { + switch (verbosity) { + case Verbosity.SHORT: + return this.decorateLabel(this.shortTitle); + case Verbosity.LONG: + return this.decorateLabel(this.longTitle); + default: + case Verbosity.MEDIUM: + return this.decorateLabel(this.mediumTitle); + } + } + + private decorateLabel(label: string): string { + const readonly = this.hasCapability(EditorInputCapabilities.Readonly); + const orphaned = !!this._modelRef?.object.isOrphaned(); + + return decorateFileEditorLabel(label, { orphaned, readonly }); + } + + public override matches(other: IEditorInput): boolean { return this === other || (other instanceof CustomEditorInput && this.viewType === other.viewType && isEqual(this.resource, other.resource)); } - override copy(): IEditorInput { + public override copy(): IEditorInput { return CustomEditorInput.create(this.instantiationService, this.resource, this.viewType, this.group, this.webview.options); } - @memoize - private get shortTitle(): string { - return this.getName(); - } - - @memoize - private get mediumTitle(): string { - return this.labelService.getUriLabel(this.resource, { relative: true }); - } - - @memoize - private get longTitle(): string { - return this.labelService.getUriLabel(this.resource); - } - - public override getTitle(verbosity?: Verbosity): string { - switch (verbosity) { - case Verbosity.SHORT: - return this.decorateLabel(this.shortTitle); - default: - case Verbosity.MEDIUM: - return this.decorateLabel(this.mediumTitle); - case Verbosity.LONG: - return this.decorateLabel(this.longTitle); - } - } - - private decorateLabel(label: string): string { - const orphaned = !!this._modelRef?.object.isOrphaned(); - - const readonly = this._modelRef - ? this._modelRef.object.isEditable() && this._modelRef.object.isOnReadonlyFileSystem() - : false; - - return decorateFileEditorLabel(label, { - orphaned, - readonly - }); - } - - public override isReadonly(): boolean { - return this._modelRef ? !this._modelRef.object.isEditable() : false; - } - - public override isUntitled(): boolean { - return this.resource.scheme === Schemas.untitled; - } - public override isDirty(): boolean { if (!this._modelRef) { return !!this._defaultDirtyState; @@ -218,19 +305,27 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { } if (!this._modelRef) { + const oldCapabilities = this.capabilities; this._modelRef = this._register(assertIsDefined(await this.customEditorService.models.tryRetain(this.resource, this.viewType))); this._register(this._modelRef.object.onDidChangeDirty(() => this._onDidChangeDirty.fire())); this._register(this._modelRef.object.onDidChangeOrphaned(() => this._onDidChangeLabel.fire())); - + this._register(this._modelRef.object.onDidChangeReadonly(() => this._onDidChangeCapabilities.fire())); + // If we're loading untitled file data we should ensure it's dirty + if (this._untitledDocumentData) { + this._defaultDirtyState = true; + } if (this.isDirty()) { this._onDidChangeDirty.fire(); } + if (this.capabilities !== oldCapabilities) { + this._onDidChangeCapabilities.fire(); + } } return null; } - override rename(group: GroupIdentifier, newResource: URI): { editor: IEditorInput } | undefined { + public override rename(group: GroupIdentifier, newResource: URI): { editor: IEditorInput } | undefined { // See if we can keep using the same custom editor provider const editorInfo = this.customEditorService.getCustomEditor(this.viewType); if (editorInfo?.matches(newResource)) { @@ -242,7 +337,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { private doMove(group: GroupIdentifier, newResource: URI): IEditorInput { if (!this._moveHandler) { - return CustomEditorInput.create(this.instantiationService, newResource, this.viewType, group); + return CustomEditorInput.create(this.instantiationService, newResource, this.viewType, group, { oldResource: this.resource }); } this._moveHandler(newResource); @@ -284,14 +379,23 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { return other; } - get backupId(): string | undefined { + public get backupId(): string | undefined { if (this._modelRef) { return this._modelRef.object.backupId; } return this._backupId; } - get untitledDocumentData(): VSBuffer | undefined { + public get untitledDocumentData(): VSBuffer | undefined { return this._untitledDocumentData; } + + public override asResourceEditorInput(groupId: GroupIdentifier): IResourceEditorInput { + return { + resource: this.resource, + options: { + override: this.viewType + } + }; + } } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts index 9507b492f9..4e1cdf6c88 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts @@ -5,13 +5,18 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ICustomEditorInputFactory, IEditorInput } from 'vs/workbench/common/editor'; import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; import { IWebviewService, WebviewContentOptions, WebviewContentPurpose, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; import { SerializedWebviewOptions, DeserializedWebview, reviveWebviewExtensionDescription, SerializedWebview, WebviewEditorInputSerializer, restoreWebviewContentOptions, restoreWebviewOptions } from 'vs/workbench/contrib/webviewPanel/browser/webviewEditorInputSerializer'; import { IWebviewWorkbenchService } from 'vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; -import { IWorkingCopyBackupMeta, NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopyBackupMeta } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; +import { ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { Schemas } from 'vs/base/common/network'; +import { isEqual } from 'vs/base/common/resources'; export interface CustomDocumentBackupData extends IWorkingCopyBackupMeta { readonly viewType: string; @@ -36,14 +41,12 @@ interface SerializedCustomEditor extends SerializedWebview { readonly backupId?: string; } - interface DeserializedCustomEditor extends DeserializedWebview { readonly editorResource: URI; readonly dirty: boolean; readonly backupId?: string; } - export class CustomEditorInputSerializer extends WebviewEditorInputSerializer { public static override readonly ID = CustomEditorInput.typeId; @@ -104,41 +107,63 @@ function reviveWebview(webviewService: IWebviewService, data: { id: string, stat return webview; } -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 workingCopyBackupService = accessor.get(IWorkingCopyBackupService); +export class ComplexCustomWorkingCopyEditorHandler extends Disposable implements IWorkbenchContribution { - const backup = await workingCopyBackupService.resolve({ resource, typeId: NO_TYPE_ID }); - if (!backup?.meta) { - throw new Error(`No backup found for custom editor: ${resource}`); - } + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IWorkingCopyEditorService private readonly _workingCopyEditorService: IWorkingCopyEditorService, + @IWorkingCopyBackupService private readonly _workingCopyBackupService: IWorkingCopyBackupService, + @IWebviewService private readonly _webviewService: IWebviewService, + @ICustomEditorService _customEditorService: ICustomEditorService // DO NOT REMOVE (needed on startup to register overrides properly) + ) { + super(); - const backupData = backup.meta; - const id = backupData.webview.id; - const extension = reviveWebviewExtensionDescription(backupData.extension?.id, backupData.extension?.location); - const webview = reviveWebview(webviewService, { - id, - webviewOptions: restoreWebviewOptions(backupData.webview.options), - contentOptions: restoreWebviewContentOptions(backupData.webview.options), - state: backupData.webview.state, - extension, - }); - - const editor = instantiationService.createInstance(CustomEditorInput, URI.revive(backupData.editorResource), backupData.viewType, id, webview, { backupId: backupData.backupId }); - editor.updateGroup(0); - return editor; - }); + this._installHandler(); } - public canResolveBackup(editorInput: IEditorInput, backupResource: URI): boolean { - if (editorInput instanceof CustomEditorInput) { - if (editorInput.resource.path === backupResource.path && backupResource.authority === editorInput.viewType) { - return true; - } - } + private _installHandler(): void { + this._register(this._workingCopyEditorService.registerHandler({ + handles: workingCopy => workingCopy.resource.scheme === Schemas.vscodeCustomEditor, + isOpen: (workingCopy, editor) => { + if (!(editor instanceof CustomEditorInput)) { + return false; + } - return false; + if (workingCopy.resource.authority !== editor.viewType.replace(/[^a-z0-9\-_]/gi, '-').toLowerCase()) { + return false; + } + + // The working copy stores the uri of the original resource as its query param + try { + const data = JSON.parse(workingCopy.resource.query); + const workingCopyResource = URI.from(data); + return isEqual(workingCopyResource, editor.resource); + } catch { + return false; + } + }, + createEditor: async workingCopy => { + const backup = await this._workingCopyBackupService.resolve(workingCopy); + if (!backup?.meta) { + throw new Error(`No backup found for custom editor: ${workingCopy.resource}`); + } + + const backupData = backup.meta; + const id = backupData.webview.id; + const extension = reviveWebviewExtensionDescription(backupData.extension?.id, backupData.extension?.location); + const webview = reviveWebview(this._webviewService, { + id, + webviewOptions: restoreWebviewOptions(backupData.webview.options), + contentOptions: restoreWebviewContentOptions(backupData.webview.options), + state: backupData.webview.state, + extension, + }); + + const editor = this._instantiationService.createInstance(CustomEditorInput, URI.revive(backupData.editorResource), backupData.viewType, id, webview, { backupId: backupData.backupId }); + editor.updateGroup(0); + return editor; + } + })); } -}; +} + diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index 4c67b2566d..06ff6d35b6 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -16,18 +16,19 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IStorageService } from 'vs/platform/storage/common/storage'; import * as colorRegistry from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { EditorInput, EditorExtensions, GroupIdentifier, IEditorInput, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; +import { EditorExtensions, GroupIdentifier, IEditorInput, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { CONTEXT_ACTIVE_CUSTOM_EDITOR_ID, CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CustomEditorCapabilities, CustomEditorInfo, CustomEditorInfoCollection, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { CustomEditorModelManager } from 'vs/workbench/contrib/customEditor/common/customEditorModelManager'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { ContributedEditorPriority, IEditorAssociationsRegistry, IEditorOverrideService, IEditorType, IEditorTypesHandler } from 'vs/workbench/services/editor/common/editorOverrideService'; +import { ContributedEditorPriority, IEditorOverrideService, IEditorType } from 'vs/workbench/services/editor/common/editorOverrideService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { ContributedCustomEditors } from '../common/contributedCustomEditors'; import { CustomEditorInput } from './customEditorInput'; -export class CustomEditorService extends Disposable implements ICustomEditorService, IEditorTypesHandler { +export class CustomEditorService extends Disposable implements ICustomEditorService { _serviceBrand: any; private readonly _contributedEditors: ContributedCustomEditors; @@ -67,7 +68,6 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ this.updateContexts(); this._onDidChangeEditorTypes.fire(); })); - this._register(Registry.as(EditorExtensions.Associations).registerEditorTypesHandler('Custom Editor', this)); this._register(this.editorService.onDidActiveEditorChange(() => this.updateContexts())); this._register(fileService.onDidRunOperation(e => { @@ -111,7 +111,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ if (!globPattern.filenamePattern) { continue; } - this._editorOverrideDisposables.push(this._register(this.extensionContributedEditorService.registerContributionPoint( + this._editorOverrideDisposables.push(this._register(this.extensionContributedEditorService.registerEditor( globPattern.filenamePattern, { id: contributedEditor.id, diff --git a/src/vs/workbench/contrib/customEditor/common/customEditor.ts b/src/vs/workbench/contrib/customEditor/common/customEditor.ts index 79e8796fc2..d9ea30ef00 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditor.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditor.ts @@ -41,6 +41,8 @@ export interface ICustomEditorService { } export interface ICustomEditorModelManager { + getAllModels(resource: URI): Promise + get(resource: URI, viewType: string): Promise; tryRetain(resource: URI, viewType: string): Promise> | undefined; @@ -55,8 +57,8 @@ export interface ICustomEditorModel extends IDisposable { readonly resource: URI; readonly backupId: string | undefined; - isEditable(): boolean; - isOnReadonlyFileSystem(): boolean; + isReadonly(): boolean; + readonly onDidChangeReadonly: Event; isOrphaned(): boolean; readonly onDidChangeOrphaned: Event; diff --git a/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts b/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts index ce4f73e3c2..5fd9726b05 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts @@ -16,6 +16,16 @@ export class CustomEditorModelManager implements ICustomEditorModelManager { counter: number }>(); + public async getAllModels(resource: URI): Promise { + const keyStart = `${resource.toString()}@@@`; + const models = []; + for (const [key, entry] of this._references) { + if (key.startsWith(keyStart) && entry.model) { + models.push(await entry.model); + } + } + return models; + } public async get(resource: URI, viewType: string): Promise { const key = this.key(resource, viewType); const entry = this._references.get(key); diff --git a/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts b/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts index 1a39ad8524..4b32e3c6df 100644 --- a/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts +++ b/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts @@ -8,7 +8,6 @@ import { Disposable, IReference } from 'vs/base/common/lifecycle'; import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; -import { FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; import { ICustomEditorModel } from 'vs/workbench/contrib/customEditor/common/customEditor'; @@ -33,12 +32,14 @@ export class CustomTextEditorModel extends Disposable implements ICustomEditorMo private readonly _onDidChangeOrphaned = this._register(new Emitter()); public readonly onDidChangeOrphaned = this._onDidChangeOrphaned.event; + private readonly _onDidChangeReadonly = this._register(new Emitter()); + public readonly onDidChangeReadonly = this._onDidChangeReadonly.event; + constructor( public readonly viewType: string, private readonly _resource: URI, private readonly _model: IReference, - @ITextFileService private readonly textFileService: ITextFileService, - @IFileService private readonly _fileService: IFileService, + @ITextFileService private readonly textFileService: ITextFileService ) { super(); @@ -47,6 +48,7 @@ export class CustomTextEditorModel extends Disposable implements ICustomEditorMo this._textFileModel = this.textFileService.files.get(_resource); if (this._textFileModel) { this._register(this._textFileModel.onDidChangeOrphaned(() => this._onDidChangeOrphaned.fire())); + this._register(this._textFileModel.onDidChangeReadonly(() => this._onDidChangeReadonly.fire())); } this._register(this.textFileService.files.onDidChangeDirty(e => { @@ -61,12 +63,8 @@ export class CustomTextEditorModel extends Disposable implements ICustomEditorMo return this._resource; } - public isEditable(): boolean { - return !this._model.object.isReadonly(); - } - - public isOnReadonlyFileSystem(): boolean { - return this._fileService.hasCapability(this._resource, FileSystemProviderCapabilities.Readonly); + public isReadonly(): boolean { + return this._model.object.isReadonly(); } public get backupId() { diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 5a1aa14823..427e359d4c 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -46,6 +46,7 @@ interface IBreakpointDecoration { } const breakpointHelperDecoration: IModelDecorationOptions = { + description: 'breakpoint-helper-decoration', glyphMarginClassName: ThemeIcon.asClassName(icons.debugBreakpointHint), stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }; @@ -94,6 +95,7 @@ function getBreakpointDecorationOptions(model: ITextModel, breakpoint: IBreakpoi const renderInline = breakpoint.column && (breakpoint.column > model.getLineFirstNonWhitespaceColumn(breakpoint.lineNumber)); return { + description: 'breakpoint-decoration', glyphMarginClassName: ThemeIcon.asClassName(icon), glyphMarginHoverMessage, stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, @@ -128,6 +130,7 @@ async function createCandidateDecorations(model: ITextModel, breakpointDecoratio result.push({ range, options: { + description: 'breakpoint-placeholder-decoration', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, beforeContentClassName: breakpointAtPosition ? undefined : `debug-breakpoint-placeholder` }, diff --git a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts index 5b437cb652..317b7935cc 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts @@ -27,7 +27,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { ITextModel } from 'vs/editor/common/model'; import { provideSuggestionItems, CompletionOptions } from 'vs/editor/contrib/suggest/suggest'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { transparent, editorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { editorForeground } from 'vs/platform/theme/common/colorRegistry'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IDecorationOptions } from 'vs/editor/common/editorCommon'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; @@ -57,7 +57,7 @@ function isCurlyBracketOpen(input: IActiveCodeEditor): boolean { } function createDecorations(theme: IColorTheme, placeHolder: string): IDecorationOptions[] { - const transparentForeground = transparent(editorForeground, 0.4)(theme); + const transparentForeground = theme.getColor(editorForeground)?.transparent(0.4); return [{ range: { startLineNumber: 0, @@ -125,7 +125,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi this.dispose(); } })); - this.codeEditorService.registerDecorationType(DECORATION_KEY, {}); + this.codeEditorService.registerDecorationType('breakpoint-widget', DECORATION_KEY, {}); this.create(); } @@ -229,7 +229,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi const setDecorations = () => { const value = this.input.getModel().getValue(); const decorations = !!value ? [] : createDecorations(this.themeService.getColorTheme(), this.placeholder); - this.input.setDecorations(DECORATION_KEY, decorations); + this.input.setDecorations('breakpoint-widget', DECORATION_KEY, decorations); }; this.input.getModel().onDidChangeContent(() => setDecorations()); this.themeService.onDidColorThemeChange(() => setDecorations()); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 908f0ec44b..62c531f18a 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -42,6 +42,7 @@ import { createAndFillInContextMenuActions, createAndFillInActionBarActions } fr import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Codicon } from 'vs/base/common/codicons'; +import { equals } from 'vs/base/common/arrays'; const $ = dom.$; @@ -76,6 +77,7 @@ export class BreakpointsView extends ViewPane { private breakpointSupportsCondition: IContextKey; private _inputBoxData: InputBoxData | undefined; breakpointInputFocused: IContextKey; + private autoFocusedIndex = -1; constructor( options: IViewletViewOptions, @@ -102,6 +104,7 @@ export class BreakpointsView extends ViewPane { this.breakpointSupportsCondition = CONTEXT_BREAKPOINT_SUPPORTS_CONDITION.bindTo(contextKeyService); this.breakpointInputFocused = CONTEXT_BREAKPOINT_INPUT_FOCUSED.bindTo(contextKeyService); this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.onBreakpointsChange())); + this._register(this.debugService.onDidChangeState(() => this.onStateChange())); } override renderBody(container: HTMLElement): void { @@ -254,6 +257,35 @@ export class BreakpointsView extends ViewPane { } } + private onStateChange(): void { + const thread = this.debugService.getViewModel().focusedThread; + let found = false; + if (thread && thread.stoppedDetails && thread.stoppedDetails.hitBreakpointIds && thread.stoppedDetails.hitBreakpointIds.length > 0) { + const hitBreakpointIds = thread.stoppedDetails.hitBreakpointIds; + const elements = this.elements; + const index = elements.findIndex(e => { + const id = e.getIdFromAdapter(thread.session.getId()); + return typeof id === 'number' && hitBreakpointIds.indexOf(id) !== -1; + }); + if (index >= 0) { + this.list.setFocus([index]); + this.list.setSelection([index]); + found = true; + this.autoFocusedIndex = index; + } + } + if (!found) { + // Deselect breakpoint in breakpoint view when no longer stopped on it #125528 + const focus = this.list.getFocus(); + const selection = this.list.getSelection(); + if (this.autoFocusedIndex >= 0 && equals(focus, selection) && focus.indexOf(this.autoFocusedIndex) >= 0) { + this.list.setFocus([]); + this.list.setSelection([]); + } + this.autoFocusedIndex = -1; + } + } + private get elements(): BreakpointItem[] { const model = this.debugService.getModel(); const elements = (>model.getExceptionBreakpoints()).concat(model.getFunctionBreakpoints()).concat(model.getDataBreakpoints()).concat(model.getBreakpoints()); diff --git a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts index 596df6c8f9..2bd578eb5d 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts @@ -24,6 +24,7 @@ const stickiness = TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; // we need a separate decoration for glyph margin, since we do not want it on each line of a multi line statement. const TOP_STACK_FRAME_MARGIN: IModelDecorationOptions = { + description: 'top-stack-frame-margin', glyphMarginClassName: ThemeIcon.asClassName(debugStackframe), stickiness, overviewRuler: { @@ -32,6 +33,7 @@ const TOP_STACK_FRAME_MARGIN: IModelDecorationOptions = { } }; const FOCUSED_STACK_FRAME_MARGIN: IModelDecorationOptions = { + description: 'focused-stack-frame-margin', glyphMarginClassName: ThemeIcon.asClassName(debugStackframeFocused), stickiness, overviewRuler: { @@ -40,14 +42,17 @@ const FOCUSED_STACK_FRAME_MARGIN: IModelDecorationOptions = { } }; const TOP_STACK_FRAME_DECORATION: IModelDecorationOptions = { + description: 'top-stack-frame-decoration', isWholeLine: true, className: 'debug-top-stack-frame-line', stickiness }; const TOP_STACK_FRAME_INLINE_DECORATION: IModelDecorationOptions = { + description: 'top-stack-frame-inline-decoration', beforeContentClassName: 'debug-top-stack-frame-column' }; const FOCUSED_STACK_FRAME_DECORATION: IModelDecorationOptions = { + description: 'focused-stack-frame-decoration', isWholeLine: true, className: 'debug-focused-stack-frame-line', stickiness diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 17525d83c1..4faceb1d3c 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -165,7 +165,7 @@ export class CallStackView extends ViewPane { const thread = sessions.length === 1 && sessions[0].getAllThreads().length === 1 ? sessions[0].getAllThreads()[0] : undefined; if (thread && thread.stoppedDetails) { this.stateMessageLabel.textContent = thread.stateLabel; - this.stateMessageLabel.title = thread.stateLabel; + this.stateMessageLabel.title = thread.stoppedDetails.text || thread.stateLabel; this.stateMessageLabel.classList.toggle('exception', thread.stoppedDetails.reason === 'exception'); this.stateMessage.hidden = false; } else if (sessions.length === 1 && sessions[0].state === State.Running) { @@ -603,6 +603,7 @@ class ThreadsRenderer implements ICompressibleTreeRenderer(JSONExtensions.JSONContribution); +const DEBUGGERS_AVAILABLE_KEY = 'debug.debuggersavailable'; + export class AdapterManager implements IAdapterManager { private debuggers: Debugger[]; @@ -48,12 +52,15 @@ export class AdapterManager implements IAdapterManager { @IExtensionService private readonly extensionService: IExtensionService, @IContextKeyService contextKeyService: IContextKeyService, @IModeService private readonly modeService: IModeService, - @IDialogService private readonly dialogService: IDialogService + @IDialogService private readonly dialogService: IDialogService, + @IStorageService private readonly storageService: IStorageService ) { this.adapterDescriptorFactories = []; this.debuggers = []; this.registerListeners(); + const debuggersAvailable = this.storageService.getBoolean(DEBUGGERS_AVAILABLE_KEY, StorageScope.WORKSPACE, false); this.debuggersAvailable = CONTEXT_DEBUGGERS_AVAILABLE.bindTo(contextKeyService); + this.debuggersAvailable.set(debuggersAvailable); } private registerListeners(): void { @@ -91,10 +98,46 @@ export class AdapterManager implements IAdapterManager { // update the schema to include all attributes, snippets and types from extensions. const items = (launchSchema.properties!['configurations'].items); + const taskSchema = TaskDefinitionRegistry.getJsonSchema(); + const definitions: IJSONSchemaMap = { + 'common': { + properties: { + 'name': { + type: 'string', + description: nls.localize('debugName', "Name of configuration; appears in the launch configuration dropdown menu."), + default: 'Launch' + }, + 'debugServer': { + type: 'number', + description: nls.localize('debugServer', "For debug extension development only: if a port is specified VS Code tries to connect to a debug adapter running in server mode"), + default: 4711 + }, + 'preLaunchTask': { + anyOf: [taskSchema, { + type: ['string'] + }], + default: '', + defaultSnippets: [{ body: { task: '', type: '' } }], + description: nls.localize('debugPrelaunchTask', "Task to run before debug session starts.") + }, + 'postDebugTask': { + anyOf: [taskSchema, { + type: ['string'], + }], + default: '', + defaultSnippets: [{ body: { task: '', type: '' } }], + description: nls.localize('debugPostDebugTask', "Task to run after debug session ends.") + }, + 'presentation': presentationSchema, + 'internalConsoleOptions': INTERNAL_CONSOLE_OPTIONS_SCHEMA, + } + } + }; + launchSchema.definitions = definitions; items.oneOf = []; items.defaultSnippets = []; this.debuggers.forEach(adapter => { - const schemaAttributes = adapter.getSchemaAttributes(); + const schemaAttributes = adapter.getSchemaAttributes(definitions); if (schemaAttributes && items.oneOf) { items.oneOf.push(...schemaAttributes); } @@ -121,6 +164,7 @@ export class AdapterManager implements IAdapterManager { registerDebugAdapterFactory(debugTypes: string[], debugAdapterLauncher: IDebugAdapterFactory): IDisposable { debugTypes.forEach(debugType => this.debugAdapterFactories.set(debugType, debugAdapterLauncher)); this.debuggersAvailable.set(this.debugAdapterFactories.size > 0); + this.storageService.store(DEBUGGERS_AVAILABLE_KEY, this.debugAdapterFactories.size > 0, StorageScope.WORKSPACE, StorageTarget.MACHINE); this._onDidRegisterDebugger.fire(); return { @@ -247,7 +291,7 @@ export class AdapterManager implements IAdapterManager { } } - if (gettingConfigurations && candidates.length === 0) { + if ((!languageLabel || gettingConfigurations) && candidates.length === 0) { await this.activateDebuggers('onDebugInitialConfigurations'); candidates = this.debuggers.filter(dbg => dbg.hasInitialConfiguration() || dbg.hasConfigurationProvider()); } diff --git a/src/vs/workbench/contrib/debug/browser/debugColors.ts b/src/vs/workbench/contrib/debug/browser/debugColors.ts index 763748bbde..bcce288252 100644 --- a/src/vs/workbench/contrib/debug/browser/debugColors.ts +++ b/src/vs/workbench/contrib/debug/browser/debugColors.ts @@ -135,15 +135,23 @@ export function registerColors() { * Only visible when there are more active debug sessions/threads running. */ .debug-pane .debug-call-stack .thread > .state.label, - .debug-pane .debug-call-stack .session > .state.label, - .debug-pane .monaco-list-row.selected .thread > .state.label, - .debug-pane .monaco-list-row.selected .session > .state.label { + .debug-pane .debug-call-stack .session > .state.label { background-color: ${debugViewStateLabelBackgroundColor}; color: ${debugViewStateLabelForegroundColor}; } + /* State "badge" displaying the active session's current state. + * Only visible when there are more active debug sessions/threads running + * and thread paused due to a thrown exception. + */ + .debug-pane .debug-call-stack .thread > .state.label.exception, + .debug-pane .debug-call-stack .session > .state.label.exception { + background-color: ${debugViewExceptionLabelBackgroundColor}; + color: ${debugViewExceptionLabelForegroundColor}; + } + /* Info "badge" shown when the debugger pauses due to a thrown exception. */ - .debug-pane .debug-call-stack-title > .pause-message > .label.exception { + .debug-pane .call-stack-state-message > .label.exception { background-color: ${debugViewExceptionLabelBackgroundColor}; color: ${debugViewExceptionLabelForegroundColor}; } diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index ed2e5d3f7c..ae5020c228 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -66,7 +66,7 @@ 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_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"); @@ -338,7 +338,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: CONTINUE_ID, weight: KeybindingWeight.WorkbenchContrib + 10, // Use a stronger weight to get priority over start debugging F5 shortcut primary: KeyCode.F5, - when: CONTEXT_IN_DEBUG_MODE, + when: CONTEXT_DEBUG_STATE.isEqualTo('stopped'), handler: (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => { getThreadAndRun(accessor, context, thread => thread.continue()); } @@ -389,7 +389,7 @@ 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))), + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE.isEqualTo('inactive')), handler: async (accessor: ServicesAccessor, debugStartOptions?: { config?: Partial; noDebug?: boolean }) => { const debugService = accessor.get(IDebugService); let { launch, name, getConfig } = debugService.getConfigurationManager().selectedConfiguration; diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index e12ebd0a4b..70e2ca143e 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -33,7 +33,7 @@ class ToggleBreakpointAction extends EditorAction2 { id: 'editor.debug.action.toggleBreakpoint', title: { value: nls.localize('toggleBreakpointAction', "Debug: Toggle Breakpoint"), - original: 'Toggle Breakpoint', + original: 'Debug: Toggle Breakpoint', mnemonicTitle: nls.localize({ key: 'miToggleBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breakpoint") }, f1: true, diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index ff81e53583..3afa63f356 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -30,7 +30,7 @@ import { ExceptionWidget } from 'vs/workbench/contrib/debug/browser/exceptionWid 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'; +import { memoize } from 'vs/base/common/decorators'; import { IEditorHoverOptions, EditorOption } from 'vs/editor/common/config/editorOptions'; import { DebugHoverWidget } from 'vs/workbench/contrib/debug/browser/debugHover'; import { ITextModel } from 'vs/editor/common/model'; @@ -45,6 +45,8 @@ import { Event } from 'vs/base/common/event'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Expression } from 'vs/workbench/contrib/debug/common/debugModel'; +import { themeColorFromId } from 'vs/platform/theme/common/themeService'; +import { registerColor } from 'vs/platform/theme/common/colorRegistry'; const LAUNCH_JSON_REGEX = /\.vscode\/launch\.json$/; const INLINE_VALUE_DECORATION_KEY = 'inlinevaluedecoration'; @@ -52,6 +54,18 @@ const MAX_NUM_INLINE_VALUES = 100; // JS Global scope can have 700+ entries. We const MAX_INLINE_DECORATOR_LENGTH = 150; // Max string length of each inline decorator when debugging. If exceeded ... is added const MAX_TOKENIZATION_LINE_LEN = 500; // If line is too long, then inline values for the line are skipped +export const debugInlineForeground = registerColor('editor.inlineValuesForeground', { + dark: '#ffffff80', + light: '#00000080', + hc: '#ffffff80' +}, nls.localize('editor.inlineValuesForeground', "Color for the debug inline value text.")); + +export const debugInlineBackground = registerColor('editor.inlineValuesBackground', { + dark: '#ffc80033', + light: '#ffc80033', + hc: '#ffc80033' +}, nls.localize('editor.inlineValuesBackground', "Color for the debug inline value background.")); + class InlineSegment { constructor(public column: number, public text: string) { } @@ -73,18 +87,9 @@ function createInlineValueDecoration(lineNumber: number, contentText: string, co renderOptions: { after: { contentText, - backgroundColor: 'rgba(255, 200, 0, 0.2)', - margin: '10px' - }, - dark: { - after: { - color: 'rgba(255, 255, 255, 0.5)', - } - }, - light: { - after: { - color: 'rgba(0, 0, 0, 0.5)', - } + backgroundColor: themeColorFromId(debugInlineBackground), + margin: '10px', + color: themeColorFromId(debugInlineForeground) } } }; @@ -185,7 +190,6 @@ export class DebugEditorContribution implements IDebugEditorContribution { private hoverRange: Range | null = null; private mouseDown = false; private exceptionWidgetVisible: IContextKey; - private static readonly MEMOIZER = createMemoizer(); private exceptionWidget: ExceptionWidget | undefined; private configurationWidget: FloatingClickWidget | undefined; @@ -208,7 +212,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { this.toDispose = []; this.registerListeners(); this.updateConfigurationWidgetVisibility(); - this.codeEditorService.registerDecorationType(INLINE_VALUE_DECORATION_KEY, {}); + this.codeEditorService.registerDecorationType('debug-inline-value-decoration', INLINE_VALUE_DECORATION_KEY, {}); this.exceptionWidgetVisible = CONTEXT_EXCEPTION_WIDGET_VISIBLE.bindTo(contextKeyService); this.toggleExceptionWidget(); } @@ -234,7 +238,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { })); this.toDispose.push(this.editor.onKeyDown((e: IKeyboardEvent) => this.onKeyDown(e))); this.toDispose.push(this.editor.onDidChangeModelContent(() => { - DebugEditorContribution.MEMOIZER.clear(); + this._wordToLineNumbersMap = undefined; this.updateInlineValuesScheduler.schedule(); })); this.toDispose.push(this.debugService.getViewModel().onWillUpdateViews(() => this.updateInlineValuesScheduler.schedule())); @@ -247,7 +251,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { this.toggleExceptionWidget(); this.hideHoverWidget(); this.updateConfigurationWidgetVisibility(); - DebugEditorContribution.MEMOIZER.clear(); + this._wordToLineNumbersMap = undefined; await this.updateInlineValueDecorations(stackFrame); })); this.toDispose.push(this.editor.onDidScrollChange(() => { @@ -266,9 +270,12 @@ export class DebugEditorContribution implements IDebugEditorContribution { })); } - @DebugEditorContribution.MEMOIZER + private _wordToLineNumbersMap: Map | undefined = undefined; private get wordToLineNumbersMap(): Map { - return getWordToLineNumbersMap(this.editor.getModel()); + if (!this._wordToLineNumbersMap) { + this._wordToLineNumbersMap = getWordToLineNumbersMap(this.editor.getModel()); + } + return this._wordToLineNumbersMap; } private applyHoverConfiguration(model: ITextModel, stackFrame: IStackFrame | undefined): void { @@ -708,7 +715,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { allDecorations = decorationsPerScope.reduce((previous, current) => previous.concat(current), []); } - this.editor.setDecorations(INLINE_VALUE_DECORATION_KEY, allDecorations); + this.editor.setDecorations('debug-inline-value-decoration', INLINE_VALUE_DECORATION_KEY, allDecorations); } dispose(): void { diff --git a/src/vs/workbench/contrib/debug/browser/debugHover.ts b/src/vs/workbench/contrib/debug/browser/debugHover.ts index 98dac56530..95ffe4871e 100644 --- a/src/vs/workbench/contrib/debug/browser/debugHover.ts +++ b/src/vs/workbench/contrib/debug/browser/debugHover.ts @@ -262,6 +262,7 @@ export class DebugHoverWidget implements IContentWidget { } private static readonly _HOVER_HIGHLIGHT_DECORATION_OPTIONS = ModelDecorationOptions.register({ + description: 'bdebug-hover-highlight', className: 'hoverHighlight' }); diff --git a/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts b/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts index ca49c2f6f8..ac0e0ef927 100644 --- a/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts +++ b/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts @@ -33,7 +33,7 @@ export class StartDebugQuickAccessProvider extends PickerQuickAccessProvider { + protected async _getPicks(filter: string): Promise<(IQuickPickSeparator | IPickerQuickAccessItem)[]> { const picks: Array = []; picks.push({ type: 'separator', label: 'launch.json' }); diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 2829970ae1..80273e5eb7 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -276,11 +276,7 @@ export class DebugService implements IDebugService { */ async startDebugging(launch: ILaunch | undefined, configOrName?: IConfig | string, options?: IDebugSessionOptions): Promise { const message = options && options.noDebug ? nls.localize('runTrust', "Running executes build tasks and program code from your workspace.") : nls.localize('debugTrust', "Debugging executes build tasks and program code from your workspace."); - const trust = await this.workspaceTrustRequestService.requestWorkspaceTrust({ - modal: true, - message, - - }); + const trust = await this.workspaceTrustRequestService.requestWorkspaceTrust({ message }); if (!trust) { return false; } @@ -289,8 +285,7 @@ export class DebugService implements IDebugService { // make sure to save all files and that the configuration is up to date await this.extensionService.activateByEvent('onDebug'); if (!options?.parentSession) { - const saveBeforeStartConfig: string = this.configurationService.getValue('debug.saveBeforeStart'); - + const saveBeforeStartConfig: string = this.configurationService.getValue('debug.saveBeforeStart', { overrideIdentifier: this.editorService.activeTextEditorMode }); if (saveBeforeStartConfig !== 'none') { await this.editorService.saveAll(); if (saveBeforeStartConfig === 'allEditorsInActiveGroup') { diff --git a/src/vs/workbench/contrib/debug/browser/debugStatus.ts b/src/vs/workbench/contrib/debug/browser/debugStatus.ts index cc1d0dbd6a..9d30b7b6d0 100644 --- a/src/vs/workbench/contrib/debug/browser/debugStatus.ts +++ b/src/vs/workbench/contrib/debug/browser/debugStatus.ts @@ -23,7 +23,7 @@ export class DebugStatusContribution implements IWorkbenchContribution { ) { const addStatusBarEntry = () => { - this.entryAccessor = this.statusBarService.addEntry(this.entry, 'status.debug', nls.localize('status.debug', "Debug"), StatusbarAlignment.LEFT, 30 /* Low Priority */); + this.entryAccessor = this.statusBarService.addEntry(this.entry, 'status.debug', StatusbarAlignment.LEFT, 30 /* Low Priority */); }; const setShowInStatusBar = () => { @@ -65,6 +65,7 @@ export class DebugStatusContribution implements IWorkbenchContribution { } return { + name: nls.localize('status.debug', "Debug"), text: '$(debug-alt-small) ' + text, ariaLabel: nls.localize('debugTarget', "Debug: {0}", text), tooltip: nls.localize('selectAndStartDebug', "Select and start debug configuration"), diff --git a/src/vs/workbench/contrib/debug/browser/media/continue-tb.png b/src/vs/workbench/contrib/debug/browser/media/continue-tb.png index fe7679985333906d97237f382de06a47f50731e8..8e2d11acd7bafe0ee1345b347dfcb03aa38dec61 100644 GIT binary patch delta 594 zcmV-Y0Wab7{>8uCX@v$j$m_vVpc4WNKeAY1)!@c zAQVRkBC;sZ13 z+O-#MC67{Lbd1F(_Meuu`<%5|-qNk4m2Is*jL)bYbJk`(!y(jW|IqSXn|<&JU$wmr zCRF>Pxx`xA>wjRfnQY?v)f-g%3K(j8A51pX2GyQ&)DB|siT|nH&}(l|LQsRrd9^KY z86^Zam<;RyN0`O#zWOFAb^!sQp$A_NFy0^C*V?q*TliE>cLVbybI8#(f>8 zsxOw;)k`5f;Jhq(t6EfPVFOKPar)QO-m4bX@O5c7(#YG;ivA^&g;13R4NoU&shf2n ggb+dqAw)QS10bPvlglRIkpKVy07*qoM6N<$f`vLFZ2$lO delta 262 zcmV+h0r~!%1=9kMReu2JNklAII*s$#d794cYx zy(M3uV6Kr$iBeG-sw_$qL>u80DaQi+0%`gMVsk1|rPxNfRF&$z^;lC}Y{%Z2JdeGD z7h}TOW_MAvKMD42W_SMcpSNcKAS5ItBqSsxBqSu{Lb95>n}1x*89H1L7O z5|Vgn;-`$o@o#^Dfbm4|TJG*NaQ^Fwk1^P`toq7Q!Z0BqICy{nKHBZRlrYPAG_zK? zKLY{biQqHPQTp}|d-t}2=-WXEZW5H+{s?9=D}-3-@X|%LT)3)bS@pMLfPOPLrn(~U=;emmnMz#ZWj6=BY!hcg=!4r>Ipa}26k8l`b^ua(5)8K(kEknB+s);p^Qclz0 z^5fq9H1r6$x|&;!RVoJ=3F2Y5rc3t^Mg3Fg16Mm9REV9z&e-tKaOu|jx8$SekDx5} z(NH+QvTL;l-@T*=j3{$TARnTce?b| zb8*#)E5TCoITYP=j0(+EyTHKy?dVmLns^t?gGMQfX_IyzR}_U+pHmmL!&*GTA>H+} z=YQm?6IX&AjvY1A77YHYmr|$Y;~KO)YE$fN5n5EsGs`MD`s}&8>ZGS_+U68QqjH~6 zU+$cfTmAR{G~;|8JaW}ZkMl6s9TyVZV^^%yJ0HAR-IsMOlEu@{ymHlvE5W$DYaXj` zcytdHELcJm&dMu+#Wl{-o$sRcGV#yH41Y%QR(4-lA9>}f6IX(1ElWqvv~guDiFLDN z#BKn}V6H_zuUci@eq=O%wdP~bg(_SLZZF(>X~kW@6&Sv2+P*R%0NnOrr_w?G9yMT% z4UQ|pIF(RbiVN=u{*kQJA0rwYfga&ZUjO>T<=-GcTm=s_Z_>{tXl$!{mXwo*G=E;1 zkJiVWvsrcBuo`C$a$eJ8v@}A4OP6QC6+9b$EXFXsG?&Xl{44@ey5 z`>blCv8U&sfNy697`*uI@?=K8ZxmPM%TE`Vs7}}(ZFj51&n{72Zy%poq2^(qJ3Tq2 z8hMZYrvBP>afzYu_U5m9eeL`vbXGT4Ki_kNf1$9v)SBn@dynWR3a{tAmc8f5dPQM* z;~nL>e~v8Yx%Rqzq458t5AnkB%YRCIWjuTCNOgsz=j#hoKVR78y=DHqtp}~jRv&(2 zyW9UF53ioepFd&e6NR?zIT*khT~?r)xW=Bp>4V5R!^0nhwk_dNPF!surk;43=cxCM jCVvQ;0rrJNhky-3R6y&TCk=9U8Gyjk)z4*}Q$iB}d}fG9 diff --git a/src/vs/workbench/contrib/debug/browser/media/pause-tb.png b/src/vs/workbench/contrib/debug/browser/media/pause-tb.png index f564b78761f723604eabc4d4f4de6cc38b1357fd..079c9d25f5f88acca3d8bd7e68eebe2d64a1418e 100644 GIT binary patch delta 221 zcmcb~xSDA~Sp5M{7srqa#&@*Xk}aC>-Pu-kx1x*?*0H-T9zYlWcm0hKVb1m;bw zqK58?-F+U0XP?%FCOs9g-#(221ZK=zaEj0H?bI!r_8hzBg+Kh3{Z3-?yh~=PZ$Hhl z-9Ek4V&$TlTAO|K-iS>9`Smot<(Gdwe)m2D d14F@e9s^~s&y&@p)&hA9p00i_EbE*SngEF^J?Q`d diff --git a/src/vs/workbench/contrib/debug/browser/media/restart-tb.png b/src/vs/workbench/contrib/debug/browser/media/restart-tb.png index 619de170ed39bc17b8be7f04da9c7e7c563f2251..9ec0196946a09745c8db987bfffde35bf27cb605 100644 GIT binary patch delta 1207 zcmV;o1W5b61^fw+ReuDLNklQdJ=ZH7KeJ_IhXfjtymT zZDMA}R*~i>8N9Rh&ODxZ=lwH)#bU8oEEbE!VzG1qLmycwEq^6^AQ9oX> z+$#LClXUjcW3&W2CFe>MxN>}51@=1)cDeK*#Uqg^CS^(nzSCheTaau+h}zS+q~oFo z3_yb@BGa7nh2!g(#0LFK2S3vg%6X37#vvtSHxHIb(I5KWpj`PWv*_1rdDFr<46|HX zj%R;dDw5bDlz&(~m=w|>;e|};!3`M1s9+_Qb5U&0R0pIi(}ZuFyyOi zW_tmK&|1>%P#Y3RY&$zuqNHT`D!k2hww`;Q!|wj?DGoLX&Th8cRbA+-w@lH{SA5R|t`wOWxl?7Zha?+vV*zhkx(15iTLv%xrRg9U(BYq?=1P ze!7iq9tbbltysl48^>I7&K-L5tw~?VnU>oOT&^}&QV5K1T1{krHZlL1u3x?L?j2kn z99a_rw_0v7$?+{h&u*3I2lY@sjYy zkA8Kxi&rSFRj;8AwiRqs%NiQ=H9gctN(bv@jDOVl;Ge-}A-uQGn_T?wZCJsWqt>;N zT=_x>Hpzt%EUH~))UV}22!6ct7>82+XOq$?4qOPqyU|_BV`VDX#x&ick$hO?vo9+z z8$15Y^%*m3FF@t53L`!Zx)cHN6dHt@VYSp$tV_xJc+Ib@dE#(YwV6cym_R)hjHRJG zm4Cn{GzcZw^?pf6;T0%eE>N;MMZ=h;wxmOT(-eEJ{$TX{)|AktJiGrhJP6Na5@W%( zM!rfx0t}FSmSqA=Wo7YT7i7T>GzlGPWlG;nM>tzM^-zWlCNJIk=!1$J*Bo2+=PX0j zr@U9@T5kP1J9h6KXcBsto=<+cPBC>c*ndXXlY)6qw#yiOwDSn(LDjWZcQTAs#Z()y zDxCZTAutk4AC+ed^mer6mP=hMvdBPw7{l_#doSR0m~pi^?Y-J#zNp7}M6OX1+?xK; zQWlG|WA{TrCM_%86bO4&>-SxoI4p4+vpF5X^4iua8y?g^L!P+ z9M`qATu?iDmJctNmL{pb^%a^fem-$JCv(EfTXt0;0BuKJ>w4uFV2X)@YWC zwf>fNPi)XV-f58?){Csquc@ipmlTH#E&~MgXxlVR`V;F{E7=~dOhG7^Fm}{%Z;y{Ea5*Ppg N002ovPDHLkV1iy9DQW-! diff --git a/src/vs/workbench/contrib/debug/browser/media/stepinto-tb.png b/src/vs/workbench/contrib/debug/browser/media/stepinto-tb.png index f7a4eb230138f2996c22a2af3c37bad926a0961b..f347b6cce6caecdfb3e2b01e426349ba9ed8acd0 100644 GIT binary patch delta 626 zcmV-&0*(Em1H1*0Reu6eNklWab6vzM1HUf5V0&;{3E>N>z3*rLw2%u8L zu7wp+h3E;Ia0Ip>>T-i}g5(Ctf-k9VAM>21iler6-FdH4`hHRrk3E{1pU0k$mjI6A zIF92ujt&H4)(c*pPk+p5ABJQwCSeo{r%Z2pf*XblFFP*0?0>lMvg5*6W&K-yCDoP9 zg7LbDQ`S6Dh2IqXB1utfeCIx{z*fQZ59kwPPs&6@q6tgGm@%jlgte~HBKsHEJ!Y&) zNukVsOz9}XFAKgG8n{zC*+F_i_Ozss{v>rE0#Sr_a+*+yoPkO0!X>27kwqwTOoYc$ z2O<#dk=}kn2Y+3qn`F0hIzS|%4q9ELEoG-oQDEaMuW*JU6kdPtWKg5JSV9Vcf&1V6$w@#PLMd}kzWQj5s`3149|zj?-GnU-GN1wLCytIxv?vci3dYWyy- z3Io)ILu{&df$R*T6ua_{Yojh)9^+ZdtUZff)?Zo@R$6CGrVA2H)P=+2+&UH&f&pVF zqu;}f{cE;a!)W~z%BTw;rIqloAzN6HU|_MXRu`_qt0kVFwKY6JhUQ@kyoUj1`{9i6 zFdWJsT6p%UZbInUL&77gUDLH~55w<9lfzpW#R{-Hj^j9v<2cvNf9G=*NJKmq&Hw-a M07*qoM6N<$f9xReu3DNkl+K&`-W*6hNn8;u|%J3c>{4;sPzF6T#C&>{8~Ja?=w1OrEj9i_v+<1|6dpCSn60z5j~Xpe5R7Y;Y581Reu6fNklu~lr~PZGp(Zeq)3*%(&qEnyF23r z9LI4S$8j7T2*$iBLZ<{ea@+<+Z*v;K5L4k^5w=4>!JTf_r+4Zx>53*FO5!v^TsgEUKX>4TZPfG_GwFx^&L^5HSMu^wNBE5=i zWLjEeiFk_2(@jxH2Q85<3HMi(jYR1-;Xz7S!8|?e9dYcYIOvk;e=y6IZVE2E?zr%} z_vceW$`}D8GBN(7Ae9E6j zuaJ!pweV5iac$Iv%X2(y*tBC2W&Nckp>c_ovdM>XP1J??aXxO^HY2W3Dx)9afFDhp z@@V}V%510$k6V@Sa6vO(k>GIOV84gD@Q0jaAIDBrP@j(s&BGS(1_qeq!&0x78Z#dj zm8R6l+(QY2|y7X(=wp5}+JeZx`!l-%z9LI4S$8nrB-~!=60ck{21ET-{ N002ovPDHLkV1g!~BjNx6 delta 335 zcmV-V0kHnP1)BqqReu36Nkl;!Tzy~F8~n{5fKp)(J(}08pcwW@Gi#I(x_cW zC$VQ6m(hh-pV2+BzM^l8^$}f&-6iX_i|9gZ6~;WXtr~r_G=KNmtFX8|7mQ5wFUBM} z!&PhhyzoP`qt{`)U~FRaEZLR~j&nL@ql_)-3bycs2g6|R80;N`2Ou!?410?(wypIQ z3{5ZqeWJP?z{K_tO!pQ;b)TPMOwpX_l}=fQ-(7r8{*e6p?`!+4yCD7FwEVmRv2wOGxh>b9w>4!GLA(zGwab1D3&m9dr7RwSYI_I{hzPzu)jmn(tPd7^=%J h5D^g(5fKsDIsr*(b&IiD1@!;`002ovPDHLkV1km^oV)-4 diff --git a/src/vs/workbench/contrib/debug/browser/media/stepover-tb.png b/src/vs/workbench/contrib/debug/browser/media/stepover-tb.png index 8eca81cefb3b3f3812d2753507a3671bc1c6248a..f492f89557121d9291b6c7317b53385ba88bf233 100644 GIT binary patch delta 842 zcmV-Q1GW651d<1kReu8~Nkl#@`W!6q(*o|EHB z5j(8#!>nlWN@0iA-+gZ2vUvn35xQN(0ml>@^Rf;zA_{Mv3ECU6ETd#fv#_=*2f0T} z&gOJYQs>c_et&ZmzO%6}_x=WW#_Jy_fl>r(OX$I6-J?tQc<&v@3dl|R%Tx>B2?P3CS+WAe3pVFNFiI@j1#XLJq zVj?zc$*38ql&ZoOr^p+WgpuHN1}cmUlGbRzfMb5T8BPruKVLHmPtTu83Or)w(ww(L z>4SsqynnE;D#{@V!{T$?OmKkvL;pl&=U|I;E1cgZOV7fBSrF)`9HrZ6;csw)r`X~X03c*fy8y6S4^@}+wFM;8k&arQPX z^cUaP*IPgR!NMiL0T%)t77F4@xRbY>*SKJfD}R|aZX#z`y%VD2LUgH{c>iM{wvFrp zA!4!LX3<}V1FU~iZQWx*7x~pt_cnJhY+Kp7g^w<_YoiIeMn4@`*}7oqPb@;Q3Y)aA zG1dhaa8MZ>RMkH_Qjcsw4D$8+8I2cIY! U4gUklJ^%m!07*qoM6N<$f;3u@(EtDd delta 473 zcmV;~0Ve*E2c!g$Reu4wNklfoC2K6mf6b}T1~Vn=p}000000000001C(<%p7Kl z^B@T3HSP7MXS71>67mtb;GXDAG#1VHJ6d-q>X+E74stdN@_!ADq-e;^h(#Bi>qr+td=AK8}|qhbN+u=uxzss6x@c=-m9zqLb;3=$(8- zoINRt)L0bN*e>nk0{aiqX`2zYhSLeE=wb&A`cUn1I%DUV`R4G@#@Sq7f#{j5v2xfd z@sN{iLJ-r}#D5@>#9*&MA9{0kGF71{sxp!Ow`db^@z_0T*hkFGu!^wlojS~wHM%46 z7+Z(KT=||n$M!v}@6<`AyiRmqL`j48OaK7zUvRy}Q*=(voX8cvH8tZk%NskKr8n_yDH0u1y0m;14eGuz0RR91000000KkqO`I;OA+#c3Y@D zgmlk5YI^MU>w50o`P-##ZnoIb9~Wn9c>Z7LaR_e=?=nsQ8}@ R2tNkmdb;|#taD0e0ssakXg>e| delta 118 zcmdnSbd+&ISbmhJi(^Oyt^TNVZe2BS@h_de~h zJZ7;eqw+^+({0W?^&N8`lwo81)%$T~Uii5!KwSssajV9&+bH|8N&|Tep00i_>zopr E0PET@xBvhE diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index 5a476f8c95..e0fc044e54 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -692,7 +692,7 @@ export class RawDebugSession implements IDisposable { const label = error.urlLabel ? error.urlLabel : nls.localize('moreInfo', "More Info"); return errors.createErrorWithActions(userMessage, { actions: [new Action('debug.moreInfo', label, undefined, true, async () => { - this.openerService.open(URI.parse(url)); + this.openerService.open(URI.parse(url), { allowCommands: true }); })] }); } diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index f6d3c549df..d3b477cd4f 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -33,7 +33,7 @@ import { createAndBindHistoryNavigationWidgetScopedContextKeyService } from 'vs/ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { getSimpleEditorOptions, getSimpleCodeEditorWidgetOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { IDecorationOptions } from 'vs/editor/common/editorCommon'; -import { transparent, editorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { editorForeground, resolveColorValue } from 'vs/platform/theme/common/colorRegistry'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems'; import { CompletionContext, CompletionList, CompletionProviderRegistry, CompletionItem, completionKindFromString, CompletionItemKind, CompletionItemInsertTextRule } from 'vs/editor/common/modes'; @@ -68,6 +68,7 @@ const $ = dom.$; const HISTORY_STORAGE_KEY = 'debug.repl.history'; const FILTER_HISTORY_STORAGE_KEY = 'debug.repl.filterHistory'; +const FILTER_VALUE_STORAGE_KEY = 'debug.repl.filterValue'; const DECORATION_KEY = 'replinputdecoration'; const FILTER_ACTION_ID = `workbench.actions.treeView.repl.filter`; @@ -132,10 +133,11 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { 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.filter.filterQuery = this.filterState.filterText = this.storageService.get(FILTER_VALUE_STORAGE_KEY, StorageScope.WORKSPACE, ''); this.multiSessionRepl = CONTEXT_MULTI_SESSION_REPL.bindTo(contextKeyService); this.multiSessionRepl.set(this.isMultiSessionView); - codeEditorService.registerDecorationType(DECORATION_KEY, {}); + codeEditorService.registerDecorationType('repl-decoration', DECORATION_KEY, {}); this.registerListeners(); } @@ -271,9 +273,10 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { } getFilterStats(): { total: number, filtered: number } { + // This could be called before the tree is created when setting this.filterState.filterText value return { - total: this.tree.getNode().children.length, - filtered: this.tree.getNode().children.filter(c => c.visible).length + total: this.tree?.getNode().children.length ?? 0, + filtered: this.tree?.getNode().children.filter(c => c.visible).length ?? 0 }; } @@ -657,7 +660,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { const decorations: IDecorationOptions[] = []; if (this.isReadonly && this.replInput.hasTextFocus() && !this.replInput.getValue()) { - const transparentForeground = transparent(editorForeground, 0.4)(this.themeService.getColorTheme()); + const transparentForeground = resolveColorValue(editorForeground, this.themeService.getColorTheme())?.transparent(0.4); decorations.push({ range: { startLineNumber: 0, @@ -674,7 +677,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { }); } - this.replInput.setDecorations(DECORATION_KEY, decorations); + this.replInput.setDecorations('repl-decoration', DECORATION_KEY, decorations); } override saveState(): void { @@ -691,6 +694,12 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { } else { this.storageService.remove(FILTER_HISTORY_STORAGE_KEY, StorageScope.WORKSPACE); } + const filterValue = this.filterState.filterText; + if (filterValue) { + this.storageService.store(FILTER_VALUE_STORAGE_KEY, filterValue, StorageScope.WORKSPACE, StorageTarget.USER); + } else { + this.storageService.remove(FILTER_VALUE_STORAGE_KEY, StorageScope.WORKSPACE); + } } super.saveState(); diff --git a/src/vs/workbench/contrib/debug/browser/replViewer.ts b/src/vs/workbench/contrib/debug/browser/replViewer.ts index ad496707b3..d05a586778 100644 --- a/src/vs/workbench/contrib/debug/browser/replViewer.ts +++ b/src/vs/workbench/contrib/debug/browser/replViewer.ts @@ -394,7 +394,7 @@ export class ReplAccessibilityProvider implements IListAccessibilityProvider 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) : ''); + ", occurred {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/welcomeView.ts b/src/vs/workbench/contrib/debug/browser/welcomeView.ts index ee9ea86526..9f19befe0d 100644 --- a/src/vs/workbench/contrib/debug/browser/welcomeView.ts +++ b/src/vs/workbench/contrib/debug/browser/welcomeView.ts @@ -32,8 +32,8 @@ const CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR = new RawContextKey( export class WelcomeView extends ViewPane { - static ID = 'workbench.debug.welcome'; - static LABEL = localize('run', "Run"); + static readonly ID = 'workbench.debug.welcome'; + static readonly LABEL = localize('run', "Run"); private debugStartLanguageContext: IContextKey; private debuggerInterestedContext: IContextKey; @@ -123,7 +123,7 @@ viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { 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.", SELECT_AND_START_ID), + "[Show all automatic debug configurations](command:{0}).", SELECT_AND_START_ID), when: CONTEXT_DEBUGGERS_AVAILABLE, group: ViewContentGroups.Debug, order: 10 diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 848653af97..f767ea4c53 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -106,6 +106,7 @@ export interface IRawStoppedDetails { totalFrames?: number; allThreadsStopped?: boolean; framesErrorMessage?: string; + hitBreakpointIds?: number[]; } // model diff --git a/src/vs/workbench/contrib/debug/common/debugUtils.ts b/src/vs/workbench/contrib/debug/common/debugUtils.ts index 25a4c1173b..451ab3024c 100644 --- a/src/vs/workbench/contrib/debug/common/debugUtils.ts +++ b/src/vs/workbench/contrib/debug/common/debugUtils.ts @@ -41,7 +41,7 @@ export function filterExceptionsFromTelemetry { + const definitionId = `${this.type}:${request}`; const attributes: IJSONSchema = this.debuggerContribution.configurationAttributes[request]; const defaultRequired = ['name', 'type', 'request']; attributes.required = attributes.required && attributes.required.length ? defaultRequired.concat(attributes.required) : defaultRequired; @@ -219,64 +216,44 @@ export class Debugger implements IDebugger { errorMessage: nls.localize('debugTypeNotRecognised', "The debug type is not recognized. Make sure that you have a corresponding debug extension installed and that it is enabled."), patternErrorMessage: nls.localize('node2NotSupported', "\"node2\" is no longer supported, use \"node\" instead and set the \"protocol\" attribute to \"inspector\".") }; - properties['name'] = { - type: 'string', - description: nls.localize('debugName', "Name of configuration; appears in the launch configuration dropdown menu."), - default: 'Launch' - }; properties['request'] = { enum: [request], description: nls.localize('debugRequest', "Request type of configuration. Can be \"launch\" or \"attach\"."), }; - properties['debugServer'] = { - type: 'number', - description: nls.localize('debugServer', "For debug extension development only: if a port is specified VS Code tries to connect to a debug adapter running in server mode"), - default: 4711 - }; - properties['preLaunchTask'] = { - anyOf: [taskSchema, { - type: ['string'] - }], - default: '', - defaultSnippets: [{ body: { task: '', type: '' } }], - description: nls.localize('debugPrelaunchTask', "Task to run before debug session starts.") - }; - properties['postDebugTask'] = { - anyOf: [taskSchema, { - type: ['string'], - }], - default: '', - defaultSnippets: [{ body: { task: '', type: '' } }], - description: nls.localize('debugPostDebugTask', "Task to run after debug session ends.") - }; - properties['presentation'] = presentationSchema; - properties['internalConsoleOptions'] = INTERNAL_CONSOLE_OPTIONS_SCHEMA; - // Clear out windows, linux and osx fields to not have cycles inside the properties object - delete properties['windows']; - delete properties['osx']; - delete properties['linux']; + for (const prop in definitions['common'].properties) { + properties[prop] = { + $ref: `#/definitions/common/properties/${prop}` + }; + } + definitions[definitionId] = attributes; - const osProperties = objects.deepClone(properties); - properties['windows'] = { - type: 'object', - description: nls.localize('debugWindowsConfiguration', "Windows specific launch configuration attributes."), - properties: osProperties - }; - properties['osx'] = { - type: 'object', - description: nls.localize('debugOSXConfiguration', "OS X specific launch configuration attributes."), - properties: osProperties - }; - properties['linux'] = { - type: 'object', - description: nls.localize('debugLinuxConfiguration', "Linux specific launch configuration attributes."), - properties: osProperties - }; Object.keys(properties).forEach(name => { // Use schema allOf property to get independent error reporting #21113 ConfigurationResolverUtils.applyDeprecatedVariableMessage(properties[name]); }); - return attributes; + + const result = { + allOf: [{ + $ref: `#/definitions/${definitionId}` + }, { + properties: { + windows: { + $ref: `#/definitions/${definitionId}`, + description: nls.localize('debugWindowsConfiguration', "Windows specific launch configuration attributes.") + }, + osx: { + $ref: `#/definitions/${definitionId}`, + description: nls.localize('debugOSXConfiguration', "OS X specific launch configuration attributes.") + }, + linux: { + $ref: `#/definitions/${definitionId}`, + description: nls.localize('debugLinuxConfiguration', "Linux specific launch configuration attributes.") + } + } + }] + }; + + return result; }); } } diff --git a/src/vs/workbench/contrib/debug/node/debugAdapter.ts b/src/vs/workbench/contrib/debug/node/debugAdapter.ts index d36963c49a..0ea27fe335 100644 --- a/src/vs/workbench/contrib/debug/node/debugAdapter.ts +++ b/src/vs/workbench/contrib/debug/node/debugAdapter.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { exists } from 'vs/base/node/pfs'; +import { Promises } from 'vs/base/node/pfs'; import * as cp from 'child_process'; import * as stream from 'stream'; import * as nls from 'vs/nls'; @@ -183,7 +183,7 @@ export class ExecutableDebugAdapter extends StreamDebugAdapter { // verify executables asynchronously if (command) { if (path.isAbsolute(command)) { - const commandExists = await exists(command); + const commandExists = await Promises.exists(command); if (!commandExists) { throw new Error(nls.localize('debugAdapterBinNotFound', "Debug adapter executable '{0}' does not exist.", command)); } diff --git a/src/vs/workbench/contrib/debug/node/terminals.ts b/src/vs/workbench/contrib/debug/node/terminals.ts index 9d1dd1965d..7d2317ac11 100644 --- a/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/src/vs/workbench/contrib/debug/node/terminals.ts @@ -5,29 +5,13 @@ import * as cp from 'child_process'; import * as platform from 'vs/base/common/platform'; -import { WindowsExternalTerminalService, MacExternalTerminalService, LinuxExternalTerminalService } from 'vs/workbench/contrib/externalTerminal/node/externalTerminalService'; -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 { getDriveLetter } from 'vs/base/common/extpath'; +import { LinuxExternalTerminalService, MacExternalTerminalService, WindowsExternalTerminalService } from 'vs/platform/externalTerminal/node/externalTerminalService'; +import { IExternalTerminalService } from 'vs/platform/externalTerminal/common/externalTerminal'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; -let externalTerminalService: IExternalTerminalService | undefined = undefined; -export function runInExternalTerminal(args: DebugProtocol.RunInTerminalRequestArguments, configProvider: ExtHostConfigProvider): Promise { - if (!externalTerminalService) { - if (platform.isWindows) { - externalTerminalService = new WindowsExternalTerminalService(undefined); - } else if (platform.isMacintosh) { - externalTerminalService = new MacExternalTerminalService(undefined); - } else if (platform.isLinux) { - externalTerminalService = new LinuxExternalTerminalService(undefined); - } else { - throw new Error('external terminals not supported on this platform'); - } - } - const config = configProvider.getConfiguration('terminal'); - return externalTerminalService.runInTerminal(args.title!, args.cwd, args.args, args.env || {}, config.external || {}); -} function spawnAsPromised(command: string, args: string[]): Promise { return new Promise((resolve, reject) => { @@ -47,15 +31,35 @@ function spawnAsPromised(command: string, args: string[]): Promise { }); } +let externalTerminalService: IExternalTerminalService | undefined = undefined; + +export function runInExternalTerminal(args: DebugProtocol.RunInTerminalRequestArguments, configProvider: ExtHostConfigProvider): Promise { + if (!externalTerminalService) { + if (platform.isWindows) { + externalTerminalService = new WindowsExternalTerminalService(undefined); + } else if (platform.isMacintosh) { + externalTerminalService = new MacExternalTerminalService(undefined); + } else if (platform.isLinux) { + externalTerminalService = new LinuxExternalTerminalService(undefined); + } else { + throw new Error('external terminals not supported on this platform'); + } + } + const config = configProvider.getConfiguration('terminal'); + return externalTerminalService.runInTerminal(args.title!, args.cwd, args.args, args.env || {}, config.external || {}); +} + export function hasChildProcesses(processId: number | undefined): Promise { if (processId) { + // if shell has at least one child process, assume that shell is busy if (platform.isWindows) { - return spawnAsPromised('wmic', ['process', 'get', 'ParentProcessId']).then(stdout => { - const pids = stdout.split('\r\n'); - return pids.some(p => parseInt(p) === processId); - }, error => { - return true; + return new Promise(async (resolve) => { + // See #123296 + const windowsProcessTree = await import('windows-process-tree'); + windowsProcessTree.getProcessTree(processId, (processTree) => { + resolve(processTree.children.length > 0); + }); }); } else { return spawnAsPromised('/usr/bin/pgrep', ['-lP', String(processId)]).then(stdout => { 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 f97bcae544..5f0ff6a257 100644 --- a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts @@ -67,7 +67,7 @@ function createTwoStackFrames(session: DebugSession): { firstStackFrame: StackFr return { firstStackFrame, secondStackFrame }; } -suite.skip('Debug - CallStack', () => { +suite.skip('Debug - CallStack', () => { // {{SQL CARBON EDIT}} Skip test let model: DebugModel; let rawSession: MockRawSession; diff --git a/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts index dd9a7b4b3f..c05be953e1 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts @@ -18,7 +18,7 @@ import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; import { createMockDebugModel } from 'vs/workbench/contrib/debug/test/browser/mockDebug'; import { createMockSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; -suite.skip('Debug - ANSI Handling', () => { +suite.skip('Debug - ANSI Handling', () => { // {{SQL CARBON EDIT}} Skip test let model: DebugModel; let session: DebugSession; 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 1d057f4ffb..bc8391755c 100644 --- a/src/vs/workbench/contrib/debug/test/node/debugger.test.ts +++ b/src/vs/workbench/contrib/debug/test/node/debugger.test.ts @@ -149,20 +149,6 @@ suite('Debug - Debugger', () => { assert.deepStrictEqual(ae!.args, debuggerContribution.args); }); - test('schema attributes', () => { - const schemaAttribute = _debugger.getSchemaAttributes()![0]; - assert.notDeepStrictEqual(schemaAttribute, debuggerContribution.configurationAttributes); - Object.keys(debuggerContribution.configurationAttributes.launch).forEach(key => { - assert.deepStrictEqual((schemaAttribute)[key], (debuggerContribution.configurationAttributes.launch)[key]); - }); - - assert.strictEqual(schemaAttribute['additionalProperties'], false); - assert.strictEqual(!!schemaAttribute['properties']!['request'], true); - assert.strictEqual(!!schemaAttribute['properties']!['name'], true); - assert.strictEqual(!!schemaAttribute['properties']!['type'], true); - assert.strictEqual(!!schemaAttribute['properties']!['preLaunchTask'], true); - }); - test('merge platform specific attributes', () => { const ae = ExecutableDebugAdapter.platformAdapterExecutable([extensionDescriptor1, extensionDescriptor2], 'mock')!; assert.strictEqual(ae.command, platform.isLinux ? 'linuxRuntime' : (platform.isMacintosh ? 'osxRuntime' : 'winRuntime')); diff --git a/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts b/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts index 3bd253b0c2..9711f77294 100644 --- a/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts +++ b/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts @@ -8,24 +8,6 @@ import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import * as assert from 'assert'; import { LanguageId, LanguageIdentifier } from 'vs/editor/common/modes'; -// -// To run the emmet tests only change .vscode/launch.json -// { -// "name": "Stacks Tests", -// "type": "node", -// "request": "launch", -// "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", -// "stopOnEntry": false, -// "args": [ -// "--timeout", -// "999999", -// "--colors", -// "-g", -// "Stacks" <<<--- Emmet -// ], -// Select the 'Stacks Tests' launch config and F5 -// - class MockGrammarContributions implements IGrammarContributions { private scopeName: string; diff --git a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts index d4183d0e15..2ac681e3ac 100644 --- a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts +++ b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; diff --git a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts index d1be67bce1..5e036d9082 100644 --- a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import { Action, IAction, Separator } from 'vs/base/common/actions'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/contrib/extensions/common/extensions'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IExtensionService, IExtensionsStatus, IExtensionHostProfile } from 'vs/workbench/services/extensions/common/extensions'; @@ -36,6 +36,9 @@ import { domEvent } from 'vs/base/browser/event'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput'; +import { Action2 } from 'vs/platform/actions/common/actions'; +import { CATEGORIES } from 'vs/workbench/common/actions'; +import { DefaultIconPath } from 'vs/platform/extensionManagement/common/extensionManagement'; interface IExtensionProfileInformation { /** @@ -54,7 +57,7 @@ interface IExtensionProfileInformation { export interface IRuntimeExtension { originalIndex: number; description: IExtensionDescription; - marketplaceInfo: IExtension; + marketplaceInfo: IExtension | undefined; status: IExtensionsStatus; profileInfo?: IExtensionProfileInformation; unresponsiveProfile?: IExtensionHostProfile; @@ -265,8 +268,8 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { data.root.classList.toggle('odd', index % 2 === 1); const onError = Event.once(domEvent(data.icon, 'error')); - onError(() => data.icon.src = element.marketplaceInfo.iconUrlFallback, null, data.elementDisposables); - data.icon.src = element.marketplaceInfo.iconUrl; + onError(() => data.icon.src = element.marketplaceInfo?.iconUrlFallback || DefaultIconPath, null, data.elementDisposables); + data.icon.src = element.marketplaceInfo?.iconUrl || DefaultIconPath; if (!data.icon.complete) { data.icon.style.visibility = 'hidden'; @@ -274,7 +277,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { } else { data.icon.style.visibility = 'inherit'; } - data.name.textContent = element.marketplaceInfo.displayName; + data.name.textContent = element.marketplaceInfo?.displayName || element.description.identifier.value; data.version.textContent = element.description.version; const activationTimes = element.status.activationTimes!; @@ -427,8 +430,10 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { actions.push(new Separator()); } - actions.push(new Action('runtimeExtensionsEditor.action.disableWorkspace', nls.localize('disable workspace', "Disable (Workspace)"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo, EnablementState.DisabledWorkspace))); - actions.push(new Action('runtimeExtensionsEditor.action.disable', nls.localize('disable', "Disable"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo, EnablementState.DisabledGlobally))); + if (e.element!.marketplaceInfo) { + actions.push(new Action('runtimeExtensionsEditor.action.disableWorkspace', nls.localize('disable workspace', "Disable (Workspace)"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo!, EnablementState.DisabledWorkspace))); + actions.push(new Action('runtimeExtensionsEditor.action.disable', nls.localize('disable', "Disable"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo!, EnablementState.DisabledGlobally))); + } actions.push(new Separator()); const profileAction = this._createProfileAction(); @@ -466,18 +471,18 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { protected abstract _createProfileAction(): Action | null; } -export class ShowRuntimeExtensionsAction extends Action { - static readonly ID = 'workbench.action.showRuntimeExtensions'; - static readonly LABEL = nls.localize('showRuntimeExtensions', "Show Running Extensions"); +export class ShowRuntimeExtensionsAction extends Action2 { - constructor( - id: string, label: string, - @IEditorService private readonly _editorService: IEditorService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.showRuntimeExtensions', + title: { value: nls.localize('showRuntimeExtensions', "Show Running Extensions"), original: 'Show Running Extensions' }, + category: CATEGORIES.Developer, + f1: true + }); } - public override async run(e?: any): Promise { - await this._editorService.openEditor(RuntimeExtensionsInput.instance, { revealIfOpened: true, pinned: true }); + async run(accessor: ServicesAccessor): Promise { + await accessor.get(IEditorService).openEditor(RuntimeExtensionsInput.instance, { revealIfOpened: true, pinned: true }); } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index ba97a74e0e..5963e9737d 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -24,8 +24,8 @@ import { IExtensionManifest, IKeyBinding, IView, IViewContainer } from 'vs/platf import { ResolvedKeybinding, KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, IExtension, ExtensionContainers } from 'vs/workbench/contrib/extensions/common/extensions'; -import { /*RatingsWidget, InstallCountWidget, */RemoteBadgeWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; -import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { RemoteBadgeWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; // {{SQL CARBON EDIT}} Remove unused imports +import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { UpdateAction, ReloadAction, MaliciousStatusLabelAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, @@ -70,6 +70,7 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { Delegate } from 'vs/workbench/contrib/extensions/browser/extensionsList'; import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { attachKeybindingLabelStyler } from 'vs/platform/theme/common/styler'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; class NavBar extends Disposable { @@ -182,7 +183,6 @@ export class ExtensionEditor extends EditorPane { private layoutParticipants: ILayoutParticipant[] = []; private readonly contentDisposables = this._register(new DisposableStore()); private readonly transientDisposables = this._register(new DisposableStore()); - private readonly keybindingLabelStylers = this.contentDisposables.add(new DisposableStore()); private activeElement: IActiveElement | null = null; private editorLoadComplete: boolean = false; @@ -327,7 +327,7 @@ export class ExtensionEditor extends EditorPane { return disposables; } - override async setInput(input: ExtensionsInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override async setInput(input: ExtensionsInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { await super.setInput(input, options, context, token); if (this.template) { await this.updateTemplate(input, this.template, !!options?.preserveFocus); @@ -1300,12 +1300,11 @@ export class ExtensionEditor extends EditorPane { return false; } - this.keybindingLabelStylers.clear(); const renderKeybinding = (keybinding: ResolvedKeybinding): HTMLElement => { const element = $(''); const kbl = new KeybindingLabel(element, OS); kbl.set(keybinding); - this.keybindingLabelStylers.add(attachKeybindingLabelStyler(kbl, this.themeService)); + this.contentDisposables.add(attachKeybindingLabelStyler(kbl, this.themeService)); return element; }; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEnablementByWorkspaceTrustRequirement.ts b/src/vs/workbench/contrib/extensions/browser/extensionEnablementByWorkspaceTrustRequirement.ts deleted file mode 100644 index 9cd55e30eb..0000000000 --- a/src/vs/workbench/contrib/extensions/browser/extensionEnablementByWorkspaceTrustRequirement.ts +++ /dev/null @@ -1,36 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable } from 'vs/base/common/lifecycle'; -import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; -import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; - - -export class ExtensionEnablementByWorkspaceTrustRequirement extends Disposable implements IWorkbenchContribution { - - constructor( - @IWorkspaceTrustManagementService workspaceTrustManagementService: IWorkspaceTrustManagementService, - @IExtensionService private readonly extensionService: IExtensionService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, - ) { - super(); - - this._register(workspaceTrustManagementService.onDidChangeTrust(trusted => this.onDidChangeTrustState(trusted))); - } - - private async onDidChangeTrustState(trusted: boolean): Promise { - if (trusted) { - // Untrusted -> Trusted - await this.extensionEnablementService.updateEnablementByWorkspaceTrustRequirement(); - } else { - // Trusted -> Untrusted - this.extensionService.stopExtensionHosts(); - await this.extensionEnablementService.updateEnablementByWorkspaceTrustRequirement(); - this.extensionService.startExtensionHosts(); - } - } -} diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEnablementWorkspaceTrustTransitionParticipant.ts b/src/vs/workbench/contrib/extensions/browser/extensionEnablementWorkspaceTrustTransitionParticipant.ts new file mode 100644 index 0000000000..5356d981ec --- /dev/null +++ b/src/vs/workbench/contrib/extensions/browser/extensionEnablementWorkspaceTrustTransitionParticipant.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IWorkspaceTrustManagementService, IWorkspaceTrustTransitionParticipant } from 'vs/platform/workspace/common/workspaceTrust'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; + +export class ExtensionEnablementWorkspaceTrustTransitionParticipant extends Disposable implements IWorkbenchContribution { + constructor( + @IExtensionService extensionService: IExtensionService, + @IHostService hostService: IHostService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IWorkbenchExtensionEnablementService extensionEnablementService: IWorkbenchExtensionEnablementService, + @IWorkspaceTrustManagementService workspaceTrustManagementService: IWorkspaceTrustManagementService, + ) { + super(); + + if (workspaceTrustManagementService.workspaceTrustEnabled) { + // The extension enablement participant will be registered only after the + // workspace trust state has been initialized. There is no need to execute + // the participant as part of the initialization process, as the workspace + // trust state is initialized before starting the extension host. + workspaceTrustManagementService.workspaceTrustInitialized.then(() => { + const workspaceTrustTransitionParticipant = new class implements IWorkspaceTrustTransitionParticipant { + async participate(trusted: boolean): Promise { + if (trusted) { + // Untrusted -> Trusted + await extensionEnablementService.updateEnablementByWorkspaceTrustRequirement(); + } else { + // Trusted -> Untrusted + if (environmentService.remoteAuthority) { + hostService.reload(); + } else { + extensionService.stopExtensionHosts(); + await extensionEnablementService.updateEnablementByWorkspaceTrustRequirement(); + extensionService.startExtensionHosts(); + } + } + } + }; + + // Execute BEFORE the workspace trust transition completes + this._register(workspaceTrustManagementService.addWorkspaceTrustTransitionParticipant(workspaceTrustTransitionParticipant)); + }); + } + } +} diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts index 46832f16bc..29e58d97f9 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts @@ -18,12 +18,14 @@ import { ExperimentalRecommendations } from 'vs/workbench/contrib/extensions/bro import { WorkspaceRecommendations } from 'vs/workbench/contrib/extensions/browser/workspaceRecommendations'; import { FileBasedRecommendations } from 'vs/workbench/contrib/extensions/browser/fileBasedRecommendations'; import { KeymapRecommendations } from 'vs/workbench/contrib/extensions/browser/keymapRecommendations'; +import { LanguageRecommendations } from 'vs/workbench/contrib/extensions/browser/languageRecommendations'; import { ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { ConfigBasedRecommendations } from 'vs/workbench/contrib/extensions/browser/configBasedRecommendations'; import { StaticRecommendations } from 'sql/workbench/contrib/extensions/browser/staticRecommendations'; import { ScenarioRecommendations } from 'sql/workbench/contrib/extensions/browser/scenarioRecommendations'; import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { IExtensionRecommendation } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; // {{SQL CARBON EDIT}} +import { timeout } from 'vs/base/common/async'; type IgnoreRecommendationClassification = { recommendationReason: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -44,6 +46,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte private readonly keymapRecommendations: KeymapRecommendations; private readonly staticRecommendations: StaticRecommendations; // {{SQL CARBON EDIT}} add ours private readonly scenarioRecommendations: ScenarioRecommendations; // {{SQL CARBON EDIT}} add ours + private readonly languageRecommendations: LanguageRecommendations; public readonly activationPromise: Promise; private sessionSeed: number; @@ -72,6 +75,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte this.keymapRecommendations = instantiationService.createInstance(KeymapRecommendations); this.staticRecommendations = instantiationService.createInstance(StaticRecommendations); // {{SQL CARBON EDIT}} add ours this.scenarioRecommendations = instantiationService.createInstance(ScenarioRecommendations); // {{SQL CARBON EDIT}} add ours + this.languageRecommendations = instantiationService.createInstance(LanguageRecommendations); if (!this.isEnabled()) { this.sessionSeed = 0; @@ -98,6 +102,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte this.keymapRecommendations.activate(), this.staticRecommendations.activate(), // {{SQL CARBON EDIT}} add ours this.scenarioRecommendations.activate(), // {{SQL CARBON EDIT}} add ours + this.languageRecommendations.activate(), ]); this._register(Event.any(this.workspaceRecommendations.onDidChangeRecommendations, this.configBasedRecommendations.onDidChangeRecommendations, this.extensionRecommendationsManagementService.onDidChangeIgnoredRecommendations)(() => this._onDidChangeRecommendations.fire())); @@ -135,7 +140,8 @@ export class ExtensionRecommendationsService extends Disposable implements IExte ...this.fileBasedRecommendations.recommendations, ...this.workspaceRecommendations.recommendations, ...this.keymapRecommendations.recommendations, - ...this.staticRecommendations.recommendations // {{SQL CARBON EDIT}} add ours + ...this.staticRecommendations.recommendations, // {{SQL CARBON EDIT}} add ours + ...this.languageRecommendations.recommendations, ]; for (const { extensionId, reason } of allRecommendations) { @@ -163,7 +169,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte ...this.exeBasedRecommendations.otherRecommendations, ...this.dynamicWorkspaceRecommendations.recommendations, ...this.experimentalRecommendations.recommendations, - ...this.staticRecommendations.recommendations + ...this.staticRecommendations.recommendations // {{SQL CARBON EDIT}} ]; const extensionIds = distinct(recommendations.map(e => e.extensionId)) @@ -195,6 +201,10 @@ export class ExtensionRecommendationsService extends Disposable implements IExte return this.toExtensionRecommendations(this.keymapRecommendations.recommendations); } + getLanguageRecommendations(): string[] { + return this.toExtensionRecommendations(this.languageRecommendations.recommendations); + } + async getWorkspaceRecommendations(): Promise { if (!this.isEnabled()) { return []; @@ -243,12 +253,19 @@ export class ExtensionRecommendationsService extends Disposable implements IExte return !this.extensionRecommendationsManagementService.ignoredRecommendations.includes(extensionId.toLowerCase()); } + // for testing + protected get workbenchRecommendationDelay() { + // remote extensions might still being installed #124119 + return 5000; + } + private async promptWorkspaceRecommendations(): Promise { const allowedRecommendations = [...this.workspaceRecommendations.recommendations, ...this.configBasedRecommendations.importantRecommendations] .map(({ extensionId }) => extensionId) .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)); if (allowedRecommendations.length) { + await timeout(this.workbenchRecommendationDelay); await this.extensionRecommendationNotificationService.promptWorkspaceRecommendations(allowedRecommendations); } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 305109e793..e5016a2217 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -6,7 +6,7 @@ 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, ISubmenuItem, IMenuItem, IAction2Options } from 'vs/platform/actions/common/actions'; +import { MenuRegistry, MenuId, registerAction2, Action2, ISubmenuItem, IMenuItem, IAction2Options } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; 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'; @@ -14,7 +14,7 @@ import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsServi 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, DefaultViewsContext, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; //{{SQL CARBON EDIT}} +import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, DefaultViewsContext, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; //{{SQL CARBON EDIT}} Remove ExtensionsSortByContext 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'; @@ -23,7 +23,7 @@ import { IConfigurationRegistry, Extensions as ConfigurationExtensions, Configur import * as jsonContributionRegistry from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { ExtensionsConfigurationSchema, ExtensionsConfigurationSchemaId } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor, optional } from 'vs/platform/instantiation/common/instantiation'; import { KeymapExtensions } from 'vs/workbench/contrib/extensions/common/extensionsUtils'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { EditorDescriptor, IEditorRegistry } from 'vs/workbench/browser/editor'; @@ -49,7 +49,7 @@ import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; -import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions, CATEGORIES } from 'vs/workbench/common/actions'; +import { CATEGORIES } from 'vs/workbench/common/actions'; import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { ExtensionRecommendationNotificationService } from 'vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService'; import { IExtensionService, toExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; @@ -60,7 +60,7 @@ 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 { ExtensionEnablementByWorkspaceTrustRequirement } from 'vs/workbench/contrib/extensions/browser/extensionEnablementByWorkspaceTrustRequirement'; +import { ExtensionEnablementWorkspaceTrustTransitionParticipant } from 'vs/workbench/contrib/extensions/browser/extensionEnablementWorkspaceTrustTransitionParticipant'; import { clearSearchResultsIcon, configureRecommendedIcon, extensionsViewIcon, filterIcon, installWorkspaceRecommendedIcon, refreshIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { ExtensionsPolicy, ExtensionsPolicyKey, EXTENSION_CATEGORIES } from 'vs/platform/extensions/common/extensions'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; @@ -75,6 +75,8 @@ import { WORKSPACE_TRUST_EXTENSION_SUPPORT } from 'vs/workbench/services/workspa import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; // {{SQL CARBON EDIT}} import * as locConstants from 'sql/base/common/locConstants'; // {{SQL CARBON EDIT}} import product from 'vs/platform/product/common/product'; // {{SQL CARBON EDIT}} +import { ExtensionsCompletionItemsProvider } from 'vs/workbench/contrib/extensions/browser/extensionsCompletionItemsProvider'; +import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); @@ -258,14 +260,29 @@ CommandsRegistry.registerCommand({ description: localize('workbench.extensions.installExtension.description', "Install the given extension"), args: [ { - name: localize('workbench.extensions.installExtension.arg.name', "Extension id or VSIX resource uri"), + name: 'extensionIdOrVSIXUri', + description: localize('workbench.extensions.installExtension.arg.decription', "Extension id or VSIX resource uri"), + constraint: (value: any) => typeof value === 'string' || value instanceof URI, + }, + { + name: 'options', + description: '(optional) Options for installing the extension. Object with the following properties: ' + + '`installOnlyNewlyAddedFromExtensionPackVSIX`: When enabled, VS Code installs only newly added extensions from the extension pack VSIX. This option is considered only when installing VSIX. ', + isOptional: true, schema: { - 'type': ['object', 'string'] + 'type': 'object', + 'properties': { + 'installOnlyNewlyAddedFromExtensionPackVSIX': { + 'type': 'boolean', + 'description': localize('workbench.extensions.installExtension.option.installOnlyNewlyAddedFromExtensionPackVSIX', "When enabled, VS Code installs only newly added extensions from the extension pack VSIX. This option is considered only while installing a VSIX."), + default: false + } + } } } ] }, - handler: async (accessor, arg: string | UriComponents) => { + handler: async (accessor, arg: string | UriComponents, options?: { installOnlyNewlyAddedFromExtensionPackVSIX?: boolean }) => { const extensionManagementService = accessor.get(IExtensionManagementService); const extensionGalleryService = accessor.get(IExtensionGalleryService); try { @@ -278,7 +295,7 @@ CommandsRegistry.registerCommand({ } } else { const vsix = URI.revive(arg); - await extensionManagementService.install(vsix); + await extensionManagementService.install(vsix, { installOnlyNewlyAddedFromExtensionPack: options?.installOnlyNewlyAddedFromExtensionPackVSIX }); } } catch (e) { onUnexpectedError(e); @@ -386,6 +403,8 @@ interface IExtensionActionOptions extends IAction2Options { class ExtensionsContributions extends Disposable implements IWorkbenchContribution { + private tasExperimentService?: ITASExperimentService; + constructor( @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, @@ -396,6 +415,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi @IInstantiationService private readonly instantiationService: IInstantiationService, @IDialogService private readonly dialogService: IDialogService, @ICommandService private readonly commandService: ICommandService, + @optional(ITASExperimentService) tasExperimentService: ITASExperimentService, ) { super(); const hasGalleryContext = CONTEXT_HAS_GALLERY.bindTo(contextKeyService); @@ -418,6 +438,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi hasWebServerContext.set(true); } + this.tasExperimentService = tasExperimentService; this.registerGlobalActions(); this.registerContextMenuActions(); this.registerQuickAccessProvider(); @@ -509,7 +530,10 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi id: MenuId.CommandPalette, when: CONTEXT_HAS_GALLERY }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@category:"programming languages" @sort:installs ')) + run: async () => { + const recommended = await this.tasExperimentService?.getTreatment('recommendedLanguages'); + runAction(this.instantiationService.createInstance(SearchExtensionsAction, recommended ? '@recommended:languages ' : '@category:"programming languages" @sort:installs ')); + } }); this.registerExtensionAction({ @@ -531,7 +555,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi if (outdated.length) { return runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@outdated ')); } else { - return this.dialogService.show(Severity.Info, localize('noUpdatesAvailable', "All extensions are up to date."), []); + return this.dialogService.show(Severity.Info, localize('noUpdatesAvailable', "All extensions are up to date.")); } } }); @@ -952,10 +976,21 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi }); this.registerExtensionAction({ - id: 'workbench.extensions.action.listTrustRequiredExtensions', - title: { value: localize('showTrustRequiredExtensions', "Show Extensions Requiring Trust"), original: 'Show Extensions Requiring Trust' }, + id: LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, + title: { value: localize('showWorkspaceUnsupportedExtensions', "Show Extensions Unsupported By Workspace"), original: 'Show Extensions Unsupported By Workspace' }, category: ExtensionsLocalizedLabel, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@trustRequired')) + 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: 6, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('workspace unsupported filter', "Workspace Unsupported") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@workspaceUnsupported')) }); this.registerExtensionAction({ @@ -1403,8 +1438,8 @@ workbenchRegistry.registerWorkbenchContribution(KeymapExtensions, LifecyclePhase workbenchRegistry.registerWorkbenchContribution(ExtensionsViewletViewsContribution, LifecyclePhase.Starting); workbenchRegistry.registerWorkbenchContribution(ExtensionActivationProgress, LifecyclePhase.Eventually); workbenchRegistry.registerWorkbenchContribution(ExtensionDependencyChecker, LifecyclePhase.Eventually); -workbenchRegistry.registerWorkbenchContribution(ExtensionEnablementByWorkspaceTrustRequirement, LifecyclePhase.Restored); +workbenchRegistry.registerWorkbenchContribution(ExtensionEnablementWorkspaceTrustTransitionParticipant, LifecyclePhase.Restored); +workbenchRegistry.registerWorkbenchContribution(ExtensionsCompletionItemsProvider, LifecyclePhase.Restored); // Running Extensions -const actionRegistry = Registry.as(WorkbenchActionExtensions.WorkbenchActions); -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ShowRuntimeExtensionsAction), 'Show Running Extensions', CATEGORIES.Developer.value); +registerAction2(ShowRuntimeExtensionsAction); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 6b35739134..d5e4d04741 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -18,7 +18,7 @@ import { IGalleryExtension, IExtensionGalleryService, INSTALL_ERROR_MALICIOUS, I import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, 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 } from 'vs/platform/extensions/common/extensions'; // {{SQL CARBON EDIT}} +import { ExtensionType, ExtensionIdentifier, IExtensionDescription, IExtensionManifest, isLanguagePackExtension, getWorkpaceSupportTypeMessage } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IFileService, IFileContent } from 'vs/platform/files/common/files'; @@ -59,9 +59,10 @@ import { ILogService } from 'vs/platform/log/common/log'; import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; import { infoIcon, manageExtensionIcon, syncEnabledIcon, syncIgnoredIcon, trustIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { isWeb } from 'vs/base/common/platform'; -import { isWorkspaceTrustEnabled } from 'vs/workbench/services/workspaces/common/workspaceTrust'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import * as locConstants from 'sql/base/common/locConstants'; // {{SQL CARBON EDIT}} +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; +import { isVirtualWorkspace } from 'vs/platform/remote/common/remoteHosts'; function getRelativeDateLabel(date: Date): string { const delta = new Date().getTime() - date.getTime(); @@ -132,7 +133,7 @@ export class PromptExtensionInstallFailureAction extends Action { } if ([INSTALL_ERROR_INCOMPATIBLE, INSTALL_ERROR_MALICIOUS].includes(this.error.name)) { - await this.dialogService.show(Severity.Info, getErrorMessage(this.error), []); + await this.dialogService.show(Severity.Info, getErrorMessage(this.error)); return; } @@ -271,7 +272,7 @@ export abstract class AbstractInstallAction extends ExtensionAction { const extension = await this.install(this.extension); - if (extension && extension?.local) { + if (extension?.local) { alert(localize('installExtensionComplete', "Installing extension {0} is completed.", this.extension.displayName)); const runningExtension = await this.getRunningExtension(extension.local); if (runningExtension && !(runningExtension.activationEvents && runningExtension.activationEvents.some(activationEent => activationEent.startsWith('onLanguage')))) { @@ -1580,29 +1581,6 @@ export class SetProductIconThemeAction extends ExtensionAction { } } - -export class ShowInstalledExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showInstalledExtensions'; - static readonly LABEL = localize('showInstalledExtensions', "Show Installed Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - override run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@installed '); - viewlet.focus(); - }); - } -} export class ShowRecommendedExtensionAction extends Action { static readonly ID = 'workbench.extensions.action.showRecommendedExtension'; @@ -2143,10 +2121,12 @@ export class SystemDisabledWarningAction extends ExtensionAction { constructor( @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @ILabelService private readonly labelService: ILabelService, + @ICommandService private readonly commandService: ICommandService, + @IWorkspaceTrustManagementService private readonly workspaceTrustService: IWorkspaceTrustManagementService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionService private readonly extensionService: IExtensionService, - @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService ) { super('extensions.install', '', `${SystemDisabledWarningAction.CLASS} hide`, false); this._register(this.labelService.onDidChangeFormatters(() => this.update(), this)); @@ -2162,6 +2142,7 @@ export class SystemDisabledWarningAction extends ExtensionAction { update(): void { this.class = `${SystemDisabledWarningAction.CLASS} hide`; this.tooltip = ''; + this.enabled = false; if ( !this.extension || !this.extension.local || @@ -2171,10 +2152,17 @@ export class SystemDisabledWarningAction extends ExtensionAction { ) { return; } - if (this.extension.enablementState === EnablementState.DisabledByVirtualWorkspace) { - this.class = `${SystemDisabledWarningAction.INFO_CLASS}`; - this.tooltip = localize('disabled because of virtual workspace', "This extension has been disabled because it does not support virtual workspaces."); - return; + + if (isVirtualWorkspace(this.contextService.getWorkspace())) { + const virtualSupportType = this.extensionManifestPropertiesService.getExtensionVirtualWorkspaceSupportType(this.extension.local.manifest); + if (virtualSupportType !== true) { + this.class = `${SystemDisabledWarningAction.INFO_CLASS}`; + const details = getWorkpaceSupportTypeMessage(this.extension.local.manifest.capabilities?.virtualWorkspaces); + this.tooltip = details || (virtualSupportType === 'limited' ? + localize('extension limited because of virtual workspace', "This extension has limited features because the current workspace is virtual.") : + localize('disabled because of virtual workspace', "This extension has been disabled because it does not support virtual workspaces.")); + return; + } } if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) { if (isLanguagePackExtension(this.extension.local.manifest)) { @@ -2217,136 +2205,29 @@ export class SystemDisabledWarningAction extends ExtensionAction { return; } } - if (isWorkspaceTrustEnabled(this.configurationService) && this.extension.enablementState === EnablementState.DisabledByTrustRequirement) { + + const untrustedSupportType = this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(this.extension.local.manifest); + if (this.workspaceTrustService.workspaceTrustEnabled && untrustedSupportType !== true && !this.workspaceTrustService.isWorkpaceTrusted()) { + const untrustedDetails = getWorkpaceSupportTypeMessage(this.extension.local.manifest.capabilities?.untrustedWorkspaces); + this.enabled = true; this.class = `${SystemDisabledWarningAction.TRUST_CLASS}`; - this.tooltip = localize('extension disabled because of trust requirement', "This extension has been disabled because the current workspace is not trusted"); + this.tooltip = untrustedDetails || (untrustedSupportType === 'limited' ? + localize('extension limited because of trust requirement', "This extension has limited features because the current workspace is not trusted.") : + localize('extension disabled because of trust requirement', "This extension has been disabled because the current workspace is not trusted.")); return; } } override run(): Promise { + // Only enabled by the workspace trust version of this action + // If other actions enable, add a new member to control this + if (this.enabled) { + this.commandService.executeCommand('workbench.trust.manage'); + } return Promise.resolve(null); } } -export class DisableAllAction extends Action { - - static readonly ID = 'workbench.extensions.action.disableAll'; - static readonly LABEL = localize('disableAll', "Disable All Installed Extensions"); - - constructor( - id: string, label: string, isPrimary: boolean, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService - ) { - super(id, label); - if (isPrimary) { - this._register(this.extensionsWorkbenchService.onChange(() => this._onDidChange.fire({ enabled: this.enabled }))); - } - } - - private getExtensionsToDisable(): IExtension[] { - return this.extensionsWorkbenchService.local.filter(e => !e.isBuiltin && !!e.local && this.extensionEnablementService.isEnabled(e.local) && this.extensionEnablementService.canChangeEnablement(e.local)); - } - - override get enabled(): boolean { - return this.getExtensionsToDisable().length > 0; - } - - override run(): Promise { - return this.extensionsWorkbenchService.setEnablement(this.getExtensionsToDisable(), EnablementState.DisabledGlobally); - } -} - -export class DisableAllWorkspaceAction extends Action { - - static readonly ID = 'workbench.extensions.action.disableAllWorkspace'; - static readonly LABEL = localize('disableAllWorkspace', "Disable All Installed Extensions for this Workspace"); - - constructor( - id: string, label: string, isPrimary: boolean, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService - ) { - super(id, label); - if (isPrimary) { - this._register(Event.any(this.workspaceContextService.onDidChangeWorkbenchState, this.extensionsWorkbenchService.onChange)(() => this._onDidChange.fire({ enabled: this.enabled }))); - } - } - - private getExtensionsToDisable(): IExtension[] { - return this.extensionsWorkbenchService.local.filter(e => !e.isBuiltin && !!e.local && this.extensionEnablementService.isEnabled(e.local) && this.extensionEnablementService.canChangeEnablement(e.local)); - } - - override get enabled(): boolean { - return this.getExtensionsToDisable().length > 0; - } - - override run(): Promise { - return this.extensionsWorkbenchService.setEnablement(this.getExtensionsToDisable(), EnablementState.DisabledWorkspace); - } -} - -export class EnableAllAction extends Action { - - static readonly ID = 'workbench.extensions.action.enableAll'; - static readonly LABEL = localize('enableAll', "Enable All Extensions"); - - constructor( - id: string, label: string, isPrimary: boolean, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService - ) { - super(id, label); - if (isPrimary) { - this._register(this.extensionsWorkbenchService.onChange(() => this._onDidChange.fire({ enabled: this.enabled }))); - } - } - - private getExtensionsToEnable(): IExtension[] { - return this.extensionsWorkbenchService.local.filter(e => !!e.local && this.extensionEnablementService.canChangeEnablement(e.local) && !this.extensionEnablementService.isEnabled(e.local)); - } - - override get enabled(): boolean { - return this.getExtensionsToEnable().length > 0; - } - - override run(): Promise { - return this.extensionsWorkbenchService.setEnablement(this.getExtensionsToEnable(), EnablementState.EnabledGlobally); - } -} - -export class EnableAllWorkspaceAction extends Action { - - static readonly ID = 'workbench.extensions.action.enableAllWorkspace'; - static readonly LABEL = localize('enableAllWorkspace', "Enable All Extensions for this Workspace"); - - constructor( - id: string, label: string, isPrimary: boolean, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService - ) { - super(id, label); - if (isPrimary) { - this._register(Event.any(this.workspaceContextService.onDidChangeWorkbenchState, this.extensionsWorkbenchService.onChange)(() => this._onDidChange.fire({ enabled: this.enabled }))); - } - } - - private getExtensionsToEnable(): IExtension[] { - return this.extensionsWorkbenchService.local.filter(e => !!e.local && this.extensionEnablementService.canChangeEnablement(e.local) && !this.extensionEnablementService.isEnabled(e.local)); - } - - override get enabled(): boolean { - return this.getExtensionsToEnable().length > 0; - } - - override run(): Promise { - return this.extensionsWorkbenchService.setEnablement(this.getExtensionsToEnable(), EnablementState.EnabledWorkspace); - } -} - export class ReinstallAction extends Action { static readonly ID = 'workbench.extensions.action.reinstall'; @@ -2483,7 +2364,7 @@ export class InstallSpecificVersionOfExtensionAction extends Action { return this.extensionsWorkbenchService.installVersion(extension, version) .then(extension => { const requireReload = !(extension.local && this.extensionService.canAddExtension(toExtensionDescription(extension.local))); - const message = requireReload ? localize('InstallAnotherVersionExtensionAction.successReload', "Please reload Azure Data Studio to complete installing the extension {0}.", extension.identifier.id) + const message = requireReload ? localize('InstallAnotherVersionExtensionAction.successReload', "Please reload Azure Data Studio to complete installing the extension {0}.", extension.identifier.id) // {{SQL CARBON EDIT}} : localize('InstallAnotherVersionExtensionAction.success', "Installing the extension {0} is completed.", extension.identifier.id); const actions = requireReload ? [{ label: localize('InstallAnotherVersionExtensionAction.reloadNow', "Reload Now"), diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsCompletionItemsProvider.ts b/src/vs/workbench/contrib/extensions/browser/extensionsCompletionItemsProvider.ts new file mode 100644 index 0000000000..3b72b248cd --- /dev/null +++ b/src/vs/workbench/contrib/extensions/browser/extensionsCompletionItemsProvider.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. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { getLocation, parse } from 'vs/base/common/json'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Position } from 'vs/editor/common/core/position'; +import { ITextModel } from 'vs/editor/common/model'; +import { CompletionContext, CompletionList, CompletionProviderRegistry, CompletionItemKind, CompletionItem } from 'vs/editor/common/modes'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { Range } from 'vs/editor/common/core/range'; + + +export class ExtensionsCompletionItemsProvider extends Disposable implements IWorkbenchContribution { + constructor( + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + ) { + super(); + + this._register(CompletionProviderRegistry.register({ language: 'jsonc', pattern: '**/settings.json' }, { + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken): Promise => { + const getWordRangeAtPosition = (model: ITextModel, position: Position): Range | null => { + const wordAtPosition = model.getWordAtPosition(position); + return wordAtPosition ? new Range(position.lineNumber, wordAtPosition.startColumn, position.lineNumber, wordAtPosition.endColumn) : null; + }; + + const location = getLocation(model.getValue(), model.getOffsetAt(position)); + const range = getWordRangeAtPosition(model, position) ?? Range.fromPositions(position, position); + + // extensions.supportUntrustedWorkspaces + if (location.path[0] === 'extensions.supportUntrustedWorkspaces' && location.path.length === 2 && location.isAtPropertyKey) { + let alreadyConfigured: string[] = []; + try { + alreadyConfigured = Object.keys(parse(model.getValue())['extensions.supportUntrustedWorkspaces']); + } catch (e) {/* ignore error */ } + + return { suggestions: await this.provideSupportUntrustedWorkspacesExtensionProposals(alreadyConfigured, range) }; + } + + return { suggestions: [] }; + } + })); + } + + private async provideSupportUntrustedWorkspacesExtensionProposals(alreadyConfigured: string[], range: Range): Promise { + const suggestions: CompletionItem[] = []; + const installedExtensions = (await this.extensionManagementService.getInstalled()).filter(e => e.manifest.main); + const proposedExtensions = installedExtensions.filter(e => alreadyConfigured.indexOf(e.identifier.id) === -1); + + if (proposedExtensions.length) { + suggestions.push(...proposedExtensions.map(e => { + const text = `"${e.identifier.id}": {\n\t"supported": true,\n\t"version": "${e.manifest.version}"\n},`; + return { label: e.identifier.id, kind: CompletionItemKind.Value, insertText: text, filterText: text, range }; + })); + } else { + const text = '"vscode.csharp": {\n\t"supported": true,\n\t"version": "0.0.0"\n},'; + suggestions.push({ label: localize('exampleExtension', "Example"), kind: CompletionItemKind.Value, insertText: text, filterText: text, range }); + } + + return suggestions; + } +} diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts b/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts index 8d26cb405e..7e674032c1 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts @@ -28,7 +28,7 @@ export class InstallExtensionQuickAccessProvider extends PickerQuickAccessProvid super(InstallExtensionQuickAccessProvider.PREFIX); } - protected getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Array | Promise> { + protected _getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Array | Promise> { // Nothing typed if (!filter) { @@ -100,7 +100,7 @@ export class ManageExtensionsQuickAccessProvider extends PickerQuickAccessProvid super(ManageExtensionsQuickAccessProvider.PREFIX); } - protected getPicks(): Array { + protected _getPicks(): Array { return [{ label: localize('manage', "Press Enter to manage your extensions."), accept: () => openExtensionsViewlet(this.viewletService) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 681b514807..fd983411eb 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -22,7 +22,7 @@ import { InstallLocalExtensionsInRemoteAction, InstallRemoteExtensionsInLocalAct 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, TrustRequiredOnStartExtensionsView, TrustRequiredOnDemandExtensionsView } from 'vs/workbench/contrib/extensions/browser/extensionsViews'; +import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInFeatureExtensionsView, BuiltInThemesExtensionsView, BuiltInProgrammingLanguageExtensionsView, ServerInstalledExtensionsView, DefaultRecommendedExtensionsView, UntrustedWorkspaceUnsupportedExtensionsView, UntrustedWorkspacePartiallySupportedExtensionsView, VirtualWorkspaceUnsupportedExtensionsView, VirtualWorkspacePartiallySupportedExtensionsView } from 'vs/workbench/contrib/extensions/browser/extensionsViews'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import Severity from 'vs/base/common/severity'; @@ -54,12 +54,13 @@ import { DragAndDropObserver } from 'vs/workbench/browser/dnd'; import { URI } from 'vs/base/common/uri'; import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; +import { VirtualWorkspaceContext, WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { isWeb } from 'vs/base/common/platform'; +import { isIOS, isWeb } from 'vs/base/common/platform'; import { installLocalInRemoteIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; import * as locConstants from 'sql/base/common/locConstants'; // {{SQL CARBON EDIT}} +import { WorkspaceTrustContext } from 'vs/workbench/services/workspaces/common/workspaceTrust'; const SearchMarketplaceExtensionsContext = new RawContextKey('searchMarketplaceExtensions', false); const SearchIntalledExtensionsContext = new RawContextKey('searchInstalledExtensions', false); @@ -70,8 +71,7 @@ const HasInstalledExtensionsContext = new RawContextKey('hasInstalledEx const HasInstalledWebExtensionsContext = new RawContextKey('hasInstalledWebExtensions', false); const BuiltInExtensionsContext = new RawContextKey('builtInExtensions', false); const SearchBuiltInExtensionsContext = new RawContextKey('searchBuiltInExtensions', false); -const TrustRequiredExtensionsContext = new RawContextKey('trustRequiredExtensions', false); -const SearchTrustRequiredExtensionsContext = new RawContextKey('searchTrustRequiredExtensions', false); +const SearchUnsupportedWorkspaceExtensionsContext = new RawContextKey('searchUnsupportedWorkspaceExtensions', false); const RecommendedExtensionsContext = new RawContextKey('recommendedExtensions', false); export class ExtensionsViewletViewsContribution implements IWorkbenchContribution { @@ -104,7 +104,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio viewDescriptors.push(...this.createBuiltinExtensionsViewDescriptors()); /* Trust Required extensions views */ - viewDescriptors.push(...this.createTrustRequiredExtensionsViewDescriptors()); + viewDescriptors.push(...this.createUnsupportedWorkspaceExtensionsViewDescriptors()); Registry.as(Extensions.ViewsRegistry).registerViews(viewDescriptors, this.container); } @@ -359,13 +359,13 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio }); /* - * View used for searching trustRequired extensions + * View used for searching workspace unsupported extensions */ viewDescriptors.push({ - id: 'workbench.views.extensions.searchTrustRequired', - name: localize('trustRequired', "Trust Required"), + id: 'workbench.views.extensions.searchWorkspaceUnsupported', + name: localize('workspaceUnsupported', "Workspace Unsupported"), ctorDescriptor: new SyncDescriptor(ExtensionsListView, [{}]), - when: ContextKeyExpr.and(ContextKeyExpr.has('searchTrustRequiredExtensions')), + when: ContextKeyExpr.and(ContextKeyExpr.has('searchWorkspaceUnsupportedExtensions')), }); return viewDescriptors; @@ -420,21 +420,35 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio return viewDescriptors; } - private createTrustRequiredExtensionsViewDescriptors(): IViewDescriptor[] { + private createUnsupportedWorkspaceExtensionsViewDescriptors(): IViewDescriptor[] { const viewDescriptors: IViewDescriptor[] = []; viewDescriptors.push({ - id: 'workbench.views.extensions.trustRequiredOnStartExtensions', - name: localize('trustRequiredOnStartExtensions', "Trust Required To Enable"), - ctorDescriptor: new SyncDescriptor(TrustRequiredOnStartExtensionsView, [{}]), - when: ContextKeyExpr.has('trustRequiredExtensions'), + id: 'workbench.views.extensions.untrustedUnsupportedExtensions', + name: localize('untrustedUnsupportedExtensions', "Disabled in Restricted Mode"), + ctorDescriptor: new SyncDescriptor(UntrustedWorkspaceUnsupportedExtensionsView, [{}]), + when: ContextKeyExpr.and(WorkspaceTrustContext.IsTrusted.negate(), SearchUnsupportedWorkspaceExtensionsContext), }); viewDescriptors.push({ - id: 'workbench.views.extensions.trustRequiredOnDemandExtensions', - name: localize('trustRequiredOnDemandExtensions', "Trust Required For Features"), - ctorDescriptor: new SyncDescriptor(TrustRequiredOnDemandExtensionsView, [{}]), - when: ContextKeyExpr.has('trustRequiredExtensions'), + id: 'workbench.views.extensions.untrustedPartiallySupportedExtensions', + name: localize('untrustedPartiallySupportedExtensions', "Limited in Restricted Mode"), + ctorDescriptor: new SyncDescriptor(UntrustedWorkspacePartiallySupportedExtensionsView, [{}]), + when: ContextKeyExpr.and(WorkspaceTrustContext.IsTrusted.negate(), SearchUnsupportedWorkspaceExtensionsContext), + }); + + viewDescriptors.push({ + id: 'workbench.views.extensions.virtualUnsupportedExtensions', + name: localize('virtualUnsupportedExtensions', "Disabled in Virtual Workspaces"), + ctorDescriptor: new SyncDescriptor(VirtualWorkspaceUnsupportedExtensionsView, [{}]), + when: ContextKeyExpr.and(VirtualWorkspaceContext, SearchUnsupportedWorkspaceExtensionsContext), + }); + + viewDescriptors.push({ + id: 'workbench.views.extensions.virtualPartiallySupportedExtensions', + name: localize('virtualPartiallySupportedExtensions', "Limited in Virtual Workspaces"), + ctorDescriptor: new SyncDescriptor(VirtualWorkspacePartiallySupportedExtensionsView, [{}]), + when: ContextKeyExpr.and(VirtualWorkspaceContext, SearchUnsupportedWorkspaceExtensionsContext), }); return viewDescriptors; @@ -455,8 +469,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE private hasInstalledWebExtensionsContextKey: IContextKey; private builtInExtensionsContextKey: IContextKey; private searchBuiltInExtensionsContextKey: IContextKey; - private trustRequiredExtensionsContextKey: IContextKey; - private searchTrustRequiredExtensionsContextKey: IContextKey; + private searchWorkspaceUnsupportedExtensionsContextKey: IContextKey; private recommendedExtensionsContextKey: IContextKey; private searchDelayer: Delayer; @@ -492,8 +505,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.sortByContextKey = ExtensionsSortByContext.bindTo(contextKeyService); this.searchMarketplaceExtensionsContextKey = SearchMarketplaceExtensionsContext.bindTo(contextKeyService); this.searchInstalledExtensionsContextKey = SearchIntalledExtensionsContext.bindTo(contextKeyService); - this.trustRequiredExtensionsContextKey = TrustRequiredExtensionsContext.bindTo(contextKeyService); - this.searchTrustRequiredExtensionsContextKey = SearchTrustRequiredExtensionsContext.bindTo(contextKeyService); + this.searchWorkspaceUnsupportedExtensionsContextKey = SearchUnsupportedWorkspaceExtensionsContext.bindTo(contextKeyService); this.searchOutdatedExtensionsContextKey = SearchOutdatedExtensionsContext.bindTo(contextKeyService); this.searchEnabledExtensionsContextKey = SearchEnabledExtensionsContext.bindTo(contextKeyService); this.searchDisabledExtensionsContextKey = SearchDisabledExtensionsContext.bindTo(contextKeyService); @@ -531,6 +543,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE const header = append(this.root, $('.header')); const placeholder = localize('searchExtensions', "Search Extensions in Marketplace"); + const searchValue = this.searchViewletState['query.value'] ? this.searchViewletState['query.value'] : ''; this.searchBox = this._register(this.instantiationService.createInstance(SuggestEnabledInput, `${VIEWLET_ID}.searchbox`, header, { @@ -559,7 +572,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this._register(this.searchBox.onShouldFocusResults(() => this.focusListView(), this)); this._register(this.onDidChangeVisibility(visible => { - if (visible) { + if (visible && !isIOS) { this.searchBox!.focus(); } })); @@ -611,7 +624,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE } override focus(): void { - if (this.searchBox) { + if (this.searchBox && !isIOS) { this.searchBox.focus(); } } @@ -685,8 +698,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.searchEnabledExtensionsContextKey.set(ExtensionsListView.isEnabledExtensionsQuery(value)); this.searchDisabledExtensionsContextKey.set(ExtensionsListView.isDisabledExtensionsQuery(value)); this.searchBuiltInExtensionsContextKey.set(ExtensionsListView.isSearchBuiltInExtensionsQuery(value)); - this.trustRequiredExtensionsContextKey.set(ExtensionsListView.isTrustRequiredExtensionsQuery(value)); - this.searchTrustRequiredExtensionsContextKey.set(ExtensionsListView.isSearchTrustRequiredExtensionsQuery(value)); + this.searchWorkspaceUnsupportedExtensionsContextKey.set(ExtensionsListView.isSearchWorkspaceUnsupportedExtensionsQuery(value)); this.builtInExtensionsContextKey.set(ExtensionsListView.isBuiltInExtensionsQuery(value)); this.recommendedExtensionsContextKey.set(isRecommendedExtensionsQuery); this.searchMarketplaceExtensionsContextKey.set(!!value && !ExtensionsListView.isLocalExtensionsQuery(value) && !isRecommendedExtensionsQuery); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 16fcb5cc3a..62378fb893 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -37,7 +37,7 @@ import { alert } from 'vs/base/browser/ui/aria/aria'; import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IAction, Action, Separator, ActionRunner } from 'vs/base/common/actions'; -import { ExtensionIdentifier, IExtensionDescription, isLanguagePackExtension, ExtensionType } from 'vs/platform/extensions/common/extensions'; // {{SQL CARBON EDIT}} Add ExtensionType +import { ExtensionIdentifier, ExtensionUntrustedWorkpaceSupportType, ExtensionVirtualWorkpaceSupportType, IExtensionDescription, isLanguagePackExtension, ExtensionType } from 'vs/platform/extensions/common/extensions'; // {{SQL CARBON EDIT}} Add ExtensionType import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { IProductService } from 'vs/platform/product/common/productService'; import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon'; @@ -50,6 +50,8 @@ import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import * as locConstants from 'sql/base/common/locConstants'; // {{SQL CARBON EDIT}} +import { isVirtualWorkspace } from 'vs/platform/remote/common/remoteHosts'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; // Extensions that are automatically classified as Programming Language extensions, but should be Feature extensions const FORCE_FEATURE_EXTENSIONS = ['vscode.git', 'vscode.search-result']; @@ -122,12 +124,14 @@ export class ExtensionsListView extends ViewPane { @IExtensionManagementServerService protected readonly extensionManagementServerService: IExtensionManagementServerService, @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, @IWorkbenchExtensionManagementService protected readonly extensionManagementService: IWorkbenchExtensionManagementService, + @IWorkspaceContextService protected readonly workspaceService: IWorkspaceContextService, @IProductService protected readonly productService: IProductService, @IContextKeyService contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IOpenerService openerService: IOpenerService, @IPreferencesService private readonly preferencesService: IPreferencesService, @IStorageService private readonly storageService: IStorageService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService ) { super({ ...(viewletViewOptions as IViewPaneOptions), @@ -395,8 +399,8 @@ export class ExtensionsListView extends ViewPane { extensions = this.filterEnabledExtensions(local, runningExtensions, query, options); } - else if (/@trustRequired/i.test(value)) { - extensions = this.filterTrustRequiredExtensions(local, query, options); + else if (/@workspaceUnsupported/i.test(value)) { + extensions = this.filterWorkspaceUnsupportedExtensions(local, query, options); } return { extensions, canIncludeInstalledExtensions }; @@ -549,32 +553,44 @@ export class ExtensionsListView extends ViewPane { return this.sortExtensions(result, options); } - private filterTrustRequiredExtensions(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] { - let value = query.value; - const onStartOnly = /@trustRequired:onStart/i.test(value); - if (onStartOnly) { - value = value.replace(/@trustRequired:onStart/g, ''); - } - const onDemandOnly = /@trustRequired:onDemand/i.test(value); - if (onDemandOnly) { - value = value.replace(/@trustRequired:onDemand/g, ''); + private filterWorkspaceUnsupportedExtensions(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] { + + // shows local extensions which are restricted or disabled in the current workspace because of the extension's capability + + const inVirtualWorkspace = isVirtualWorkspace(this.workspaceService.getWorkspace()); + const inRestrictedWorkspace = !this.workspaceTrustManagementService.isWorkpaceTrusted(); + if (!inVirtualWorkspace && !inRestrictedWorkspace) { + return []; } - value = value.replace(/@trustRequired/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase(); + let queryString = query.value; // @sortby is already filtered out - const result = local.filter(extension => extension.local && this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(extension.local.manifest) !== true && (extension.name.toLowerCase().indexOf(value) > -1 || extension.displayName.toLowerCase().indexOf(value) > -1)); + const match = queryString.match(/^\s*@workspaceUnsupported(?::(untrusted|virtual)(Partial)?)?(?:\s+([^\s]*))?/i); + if (!match) { + return []; + } + const type = match[1]?.toLowerCase(); + const partial = !!match[2]; + const nameFilter = match[3]?.toLowerCase(); - if (onStartOnly) { - const onStartExtensions = result.filter(extension => extension.local && this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(extension.local.manifest) === false); - return this.sortExtensions(onStartExtensions, options); + if (nameFilter) { + local = local.filter(extension => extension.name.toLowerCase().indexOf(nameFilter) > -1 || extension.displayName.toLowerCase().indexOf(nameFilter) > -1); } - if (onDemandOnly) { - const onDemandExtensions = result.filter(extension => extension.local && this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(extension.local.manifest) === 'limited'); - return this.sortExtensions(onDemandExtensions, options); - } + const hasVirtualSupportType = (extension: IExtension, supportType: ExtensionVirtualWorkpaceSupportType) => extension.local && this.extensionManifestPropertiesService.getExtensionVirtualWorkspaceSupportType(extension.local.manifest) === supportType; + const hasRestrictedSupportType = (extension: IExtension, supportType: ExtensionUntrustedWorkpaceSupportType) => extension.local && this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(extension.local.manifest) === supportType; - return this.sortExtensions(result, options); + if (type === 'virtual') { + // show limited and disabled extensions unless disabled because of a untrusted workspace + local = local.filter(extension => inVirtualWorkspace && hasVirtualSupportType(extension, partial ? 'limited' : false) && !(inRestrictedWorkspace && hasRestrictedSupportType(extension, false))); + } else if (type === 'untrusted') { + // show limited and disabled extensions unless disabled because of a virtual workspace + local = local.filter(extension => inRestrictedWorkspace && hasRestrictedSupportType(extension, partial ? 'limited' : false) && !(inVirtualWorkspace && hasVirtualSupportType(extension, false))); + } else { + // show extensions that are restricted or disabled in the current workspace + local = local.filter(extension => inVirtualWorkspace && !hasVirtualSupportType(extension, true) || inRestrictedWorkspace && !hasRestrictedSupportType(extension, true)); + } + return this.sortExtensions(local, options); } @@ -725,6 +741,7 @@ export class ExtensionsListView extends ViewPane { private isRecommendationsQuery(query: Query): boolean { return ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value) || ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value) + || ExtensionsListView.isLanguageRecommendedExtensionsQuery(query.value) || ExtensionsListView.isExeRecommendedExtensionsQuery(query.value) || /@recommended:all/i.test(query.value) || ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value) @@ -806,6 +823,11 @@ export class ExtensionsListView extends ViewPane { return this.getKeymapRecommendationsModel(query, options, token); } + // Language recommendations + if (ExtensionsListView.isLanguageRecommendedExtensionsQuery(query.value)) { + return this.getLanguageRecommendationsModel(query, options, token); + } + // Exe recommendations if (ExtensionsListView.isExeRecommendedExtensionsQuery(query.value)) { return this.getExeRecommendationsModel(query, options, token); @@ -870,6 +892,14 @@ export class ExtensionsListView extends ViewPane { return new PagedModel(installableRecommendations); } + private async getLanguageRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + const value = query.value.replace(/@recommended:languages/g, '').trim().toLowerCase(); + const recommendations = this.extensionRecommendationsService.getLanguageRecommendations(); + const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-languages' }, token)) + .filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1); + return new PagedModel(installableRecommendations); + } + private async getExeRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { const exe = query.value.replace(/@exe:/g, '').trim().toLowerCase(); const { important, others } = await this.extensionRecommendationsService.getExeBasedRecommendations(exe.startsWith('"') ? exe.substring(1, exe.length - 1) : exe); @@ -1047,9 +1077,7 @@ export class ExtensionsListView extends ViewPane { || this.isBuiltInExtensionsQuery(query) || this.isSearchBuiltInExtensionsQuery(query) || this.isBuiltInGroupExtensionsQuery(query) - || this.isSearchTrustRequiredExtensionsQuery(query) - || this.isTrustRequiredExtensionsQuery(query) - || this.isTrustRequiredGroupExtensionsQuery(query); + || this.isSearchWorkspaceUnsupportedExtensionsQuery(query); } static isSearchBuiltInExtensionsQuery(query: string): boolean { @@ -1064,16 +1092,8 @@ export class ExtensionsListView extends ViewPane { return /^\s*@builtin:.+$/i.test(query.trim()); } - static isSearchTrustRequiredExtensionsQuery(query: string): boolean { - return /@trustRequired\s.+/i.test(query); - } - - static isTrustRequiredExtensionsQuery(query: string): boolean { - return /^\s*@trustRequired$/i.test(query.trim()); - } - - static isTrustRequiredGroupExtensionsQuery(query: string): boolean { - return /^\s*@trustRequired:.+$/i.test(query.trim()); + static isSearchWorkspaceUnsupportedExtensionsQuery(query: string): boolean { + return /^\s*@workspaceUnsupported(:(untrusted|virtual)(Partial)?)?(\s|$)/i.test(query); } static isInstalledExtensionsQuery(query: string): boolean { @@ -1112,6 +1132,10 @@ export class ExtensionsListView extends ViewPane { return /@recommended:keymaps/i.test(query); } + static isLanguageRecommendedExtensionsQuery(query: string): boolean { + return /@recommended:languages/i.test(query); + } + override focus(): void { super.focus(); if (!this.list) { @@ -1176,15 +1200,46 @@ export class BuiltInProgrammingLanguageExtensionsView extends ExtensionsListView } } -export class TrustRequiredOnStartExtensionsView extends ExtensionsListView { +function toSpecificWorkspaceUnsupportedQuery(query: string, qualifier: string): string | undefined { + if (!query) { + return '@workspaceUnsupported:' + qualifier; + } + const match = query.match(new RegExp(`@workspaceUnsupported(:${qualifier})?(\\s|$)`, 'i')); + if (match) { + if (!match[1]) { + return query.replace(/@workspaceUnsupported/gi, '@workspaceUnsupported:' + qualifier); + } + return query; + } + return undefined; +} + + +export class UntrustedWorkspaceUnsupportedExtensionsView extends ExtensionsListView { override async show(query: string): Promise> { - return (query && query.trim() !== '@trustRequired') ? this.showEmptyModel() : super.show('@trustRequired:onStart'); + const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'untrusted'); + return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel(); } } -export class TrustRequiredOnDemandExtensionsView extends ExtensionsListView { +export class UntrustedWorkspacePartiallySupportedExtensionsView extends ExtensionsListView { override async show(query: string): Promise> { - return (query && query.trim() !== '@trustRequired') ? this.showEmptyModel() : super.show('@trustRequired:onDemand'); + const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'untrustedPartial'); + return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel(); + } +} + +export class VirtualWorkspaceUnsupportedExtensionsView extends ExtensionsListView { + override async show(query: string): Promise> { + const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'virtual'); + return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel(); + } +} + +export class VirtualWorkspacePartiallySupportedExtensionsView extends ExtensionsListView { + override async show(query: string): Promise> { + const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'virtualPartial'); + return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel(); } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 58e83a43e0..cee81e0139 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -303,18 +303,12 @@ ${this.description} return this.galleryService.getChangelog(this.gallery, token); } - const changelogUrl = this.local && this.local.changelogUrl; - - if (!changelogUrl) { - if (this.type === ExtensionType.System) { - // {{SQL CARBON EDIT}} - return Promise.resolve('Please check the [Azure Data Studio Release Notes](command:update.showCurrentReleaseNotes) for changes to the built-in extensions.'); - } - - return Promise.reject(new Error('not available')); + if (this.type === ExtensionType.System) { + // {{SQL CARBON EDIT}} + return Promise.resolve('Please check the [Azure Data Studio Release Notes](command:update.showCurrentReleaseNotes) for changes to the built-in extensions.'); } - return this.fileService.readFile(changelogUrl).then(content => content.value.toString()); + return Promise.reject(new Error('not available')); } get dependencies(): string[] { @@ -591,8 +585,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (e.affectsConfiguration(AutoUpdateConfigurationKey)) { // {{SQL CARBON EDIT}} // if (this.isAutoUpdateEnabled()) { - // this.checkForUpdates(); - //} + // this.checkForUpdates(); + // } } if (e.affectsConfiguration(AutoCheckUpdatesConfigurationKey)) { if (this.isAutoCheckUpdatesEnabled()) { @@ -986,8 +980,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension // This is the execution path for install/update extension from marketplace. // Check both the vscode version and azure data studio version // The check is added here because we want to fail fast instead of downloading the VSIX and then fail. - if (gallery.properties.engine && (!isEngineValid(gallery.properties.engine, this.productService.vscodeVersion) - || (gallery.properties.azDataEngine && !isEngineValid(gallery.properties.azDataEngine, this.productService.version)))) { + if (gallery.properties.engine && (!isEngineValid(gallery.properties.engine, this.productService.vscodeVersion, this.productService.date) + || (gallery.properties.azDataEngine && !isEngineValid(gallery.properties.azDataEngine, this.productService.version, this.productService.date)))) { return Promise.reject(new Error(nls.localize('incompatible2', "Unable to install version '{2}' of extension '{0}' as it is not compatible with Azure Data Studio '{1}'.", extension.gallery!.identifier.id, this.productService.version, gallery.version))); } diff --git a/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts new file mode 100644 index 0000000000..974a2e48cb --- /dev/null +++ b/src/vs/workbench/contrib/extensions/browser/languageRecommendations.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 { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; + +export class LanguageRecommendations extends ExtensionRecommendations { + + private _recommendations: ExtensionRecommendation[] = []; + get recommendations(): ReadonlyArray { return this._recommendations; } + + constructor( + @IProductService private readonly productService: IProductService, + ) { + super(); + } + + protected async doActivate(): Promise { + if (this.productService.languageExtensionTips) { + this._recommendations = this.productService.languageExtensionTips.map(extensionId => ({ + extensionId: extensionId.toLowerCase(), + reason: { + reasonId: ExtensionRecommendationReason.Application, + reasonText: '' + } + })); + } + } + +} + diff --git a/src/vs/workbench/contrib/extensions/common/extensionQuery.ts b/src/vs/workbench/contrib/extensions/common/extensionQuery.ts index f57a4330ff..9087326693 100644 --- a/src/vs/workbench/contrib/extensions/common/extensionQuery.ts +++ b/src/vs/workbench/contrib/extensions/common/extensionQuery.ts @@ -13,7 +13,7 @@ export class Query { } static suggestions(query: string): string[] { - const commands = ['installed', 'outdated', 'enabled', 'disabled', 'builtin', 'recommended', 'trustRequired', 'sort', 'category', 'tag', 'ext', 'id'] as const; + const commands = ['installed', 'outdated', 'enabled', 'disabled', 'builtin', 'recommended', 'workspaceUnsupported', 'sort', 'category', 'tag', 'ext', 'id'] as const; const subcommands = { 'sort': ['installs', 'rating', 'name'], 'category': EXTENSION_CATEGORIES.map(c => `"${c.toLowerCase()}"`), diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 83a9b0bb39..bb659d9309 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -18,7 +18,7 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export const VIEWLET_ID = 'workbench.view.extensions'; -export const EXTENSIONS_CONFIG = '.azuredatastudio/extensions.json'; +export const EXTENSIONS_CONFIG = '.azuredatastudio/extensions.json'; // {{SQL CARBON EDIT}} export interface IExtensionsViewPaneContainer extends IViewPaneContainer { readonly searchValue: string | undefined; @@ -106,14 +106,12 @@ export interface IExtensionsWorkbenchService { export const ConfigurationKey = 'extensions'; export const AutoUpdateConfigurationKey = 'extensions.autoUpdate'; export const AutoCheckUpdatesConfigurationKey = 'extensions.autoCheckUpdates'; -export const ShowRecommendationsOnlyOnDemandKey = 'extensions.showRecommendationsOnlyOnDemand'; export const CloseExtensionDetailsOnViewChangeKey = 'extensions.closeExtensionDetailsOnViewChange'; export interface IExtensionsConfiguration { autoUpdate: boolean; autoCheckUpdates: boolean; ignoreRecommendations: boolean; - showRecommendationsOnlyOnDemand: boolean; closeExtensionDetailsOnViewChange: boolean; // {{SQL CARBON EDIT}} extensionsPolicy: string; @@ -163,6 +161,8 @@ export const TOGGLE_IGNORE_EXTENSION_ACTION_ID = 'workbench.extensions.action.to export const SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID = 'workbench.extensions.action.installVSIX'; export const INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID = 'workbench.extensions.command.installFromVSIX'; +export const LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID = 'workbench.extensions.action.listWorkspaceUnsupportedExtensions'; + // Context Keys export const DefaultViewsContext = new RawContextKey('defaultExtensionViews', true); export const ExtensionsSortByContext = new RawContextKey('extensionsSortByValue', ''); diff --git a/src/vs/workbench/contrib/extensions/common/extensionsInput.ts b/src/vs/workbench/contrib/extensions/common/extensionsInput.ts index 6cb209f31a..642e5c90f4 100644 --- a/src/vs/workbench/contrib/extensions/common/extensionsInput.ts +++ b/src/vs/workbench/contrib/extensions/common/extensionsInput.ts @@ -6,7 +6,8 @@ import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { EditorInput } from 'vs/workbench/common/editor'; +import { EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IExtension } from 'vs/workbench/contrib/extensions/common/extensions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { join } from 'vs/base/common/path'; @@ -19,6 +20,10 @@ export class ExtensionsInput extends EditorInput { return ExtensionsInput.ID; } + override get capabilities(): EditorInputCapabilities { + return EditorInputCapabilities.Readonly | EditorInputCapabilities.Singleton; + } + override get resource() { return URI.from({ scheme: Schemas.extension, @@ -36,10 +41,6 @@ export class ExtensionsInput extends EditorInput { return localize('extensionsInputName', "Extension: {0}", this.extension.displayName); } - override canSplit(): boolean { - return false; - } - override matches(other: unknown): boolean { if (super.matches(other)) { return true; diff --git a/src/vs/workbench/contrib/extensions/common/runtimeExtensionsInput.ts b/src/vs/workbench/contrib/extensions/common/runtimeExtensionsInput.ts index ce16187e13..8e1724aa4b 100644 --- a/src/vs/workbench/contrib/extensions/common/runtimeExtensionsInput.ts +++ b/src/vs/workbench/contrib/extensions/common/runtimeExtensionsInput.ts @@ -5,7 +5,8 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; -import { EditorInput } from 'vs/workbench/common/editor'; +import { EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; export class RuntimeExtensionsInput extends EditorInput { @@ -15,6 +16,10 @@ export class RuntimeExtensionsInput extends EditorInput { return RuntimeExtensionsInput.ID; } + override get capabilities(): EditorInputCapabilities { + return EditorInputCapabilities.Readonly | EditorInputCapabilities.Singleton; + } + static _instance: RuntimeExtensionsInput; static get instance() { if (!RuntimeExtensionsInput._instance || RuntimeExtensionsInput._instance.isDisposed()) { @@ -33,10 +38,6 @@ export class RuntimeExtensionsInput extends EditorInput { return nls.localize('extensionsInputName', "Running Extensions"); } - override canSplit(): boolean { - return false; - } - override matches(other: unknown): boolean { return other instanceof RuntimeExtensionsInput; } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts index 9dc83cc80d..982a61ab24 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts @@ -10,11 +10,11 @@ import { IExtensionHostProfile, ProfileSession, IExtensionService } from 'vs/wor import { Disposable, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { onUnexpectedError } from 'vs/base/common/errors'; import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/workbench/services/statusbar/common/statusbar'; -import { IExtensionHostProfileService, ProfileSessionState } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor'; +import { IExtensionHostProfileService, ProfileSessionState } from 'vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { randomPort } from 'vs/base/node/ports'; +import { randomPort } from 'vs/base/common/ports'; import { IProductService } from 'vs/platform/product/common/productService'; import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; @@ -82,6 +82,7 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio if (visible) { const indicator: IStatusbarEntry = { + name: nls.localize('status.profiler', "Extension Profiler"), text: nls.localize('profilingExtensionHost', "Profiling Extension Host"), showProgress: true, ariaLabel: nls.localize('profilingExtensionHost', "Profiling Extension Host"), @@ -98,7 +99,7 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio this.profilingStatusBarIndicatorLabelUpdater.value = toDisposable(() => clearInterval(handle)); if (!this.profilingStatusBarIndicator) { - this.profilingStatusBarIndicator = this._statusbarService.addEntry(indicator, 'status.profiler', nls.localize('status.profiler', "Extension Profiler"), StatusbarAlignment.RIGHT); + this.profilingStatusBarIndicator = this._statusbarService.addEntry(indicator, 'status.profiler', StatusbarAlignment.RIGHT); } else { this.profilingStatusBarIndicator.update(indicator); } 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 36f379b529..34f997475c 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts @@ -3,136 +3,16 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { EditorDescriptor, IEditorRegistry } from 'vs/workbench/browser/editor'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { RuntimeExtensionsEditor, IExtensionHostProfileService, StartExtensionHostProfileAction, StopExtensionHostProfileAction, CONTEXT_PROFILE_SESSION_STATE, CONTEXT_EXTENSION_HOST_PROFILE_RECORDED, SaveExtensionHostProfileAction } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor'; -import { DebugExtensionHostAction } from 'vs/workbench/contrib/extensions/electron-browser/debugExtensionHostAction'; -import { EditorInput, IEditorInputSerializer, IEditorInputFactoryRegistry, ActiveEditorContext, EditorExtensions } from 'vs/workbench/common/editor'; +import { IExtensionHostProfileService } from 'vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor'; import { ExtensionHostProfileService } from 'vs/workbench/contrib/extensions/electron-browser/extensionProfileService'; -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 { 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'; -import { ISharedProcessService } from 'vs/platform/ipc/electron-sandbox/services'; -import { ExtensionRecommendationNotificationServiceChannel } from 'vs/platform/extensionRecommendations/electron-sandbox/extensionRecommendationsIpc'; -import { Codicon } from 'vs/base/common/codicons'; // Singletons registerSingleton(IExtensionHostProfileService, ExtensionHostProfileService, true); const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(ExtensionsAutoProfiler, LifecyclePhase.Eventually); - -// Running Extensions Editor -Registry.as(EditorExtensions.Editors).registerEditor( - EditorDescriptor.create(RuntimeExtensionsEditor, RuntimeExtensionsEditor.ID, localize('runtimeExtension', "Running Extensions")), - [new SyncDescriptor(RuntimeExtensionsInput)] -); - -class RuntimeExtensionsInputSerializer implements IEditorInputSerializer { - canSerialize(editorInput: EditorInput): boolean { - return true; - } - serialize(editorInput: EditorInput): string { - return ''; - } - deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput { - return RuntimeExtensionsInput.instance; - } -} - -Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputSerializer(RuntimeExtensionsInput.ID, RuntimeExtensionsInputSerializer); - - -// Global actions -const actionRegistry = Registry.as(WorkbenchActionExtensions.WorkbenchActions); - -class ExtensionsContributions implements IWorkbenchContribution { - - constructor( - @IExtensionRecommendationNotificationService extensionRecommendationNotificationService: IExtensionRecommendationNotificationService, - @ISharedProcessService sharedProcessService: ISharedProcessService, - ) { - sharedProcessService.registerChannel('extensionRecommendationNotification', new ExtensionRecommendationNotificationServiceChannel(extensionRecommendationNotificationService)); - const openExtensionsFolderActionDescriptor = SyncActionDescriptor.from(OpenExtensionsFolderAction); - actionRegistry.registerWorkbenchAction(openExtensionsFolderActionDescriptor, 'Extensions: Open Extensions Folder', ExtensionsLabel); - } -} - -workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, LifecyclePhase.Starting); - -// Register Commands - -CommandsRegistry.registerCommand(DebugExtensionHostAction.ID, (accessor: ServicesAccessor) => { - const instantiationService = accessor.get(IInstantiationService); - instantiationService.createInstance(DebugExtensionHostAction).run(); -}); - -CommandsRegistry.registerCommand(StartExtensionHostProfileAction.ID, (accessor: ServicesAccessor) => { - const instantiationService = accessor.get(IInstantiationService); - instantiationService.createInstance(StartExtensionHostProfileAction, StartExtensionHostProfileAction.ID, StartExtensionHostProfileAction.LABEL).run(); -}); - -CommandsRegistry.registerCommand(StopExtensionHostProfileAction.ID, (accessor: ServicesAccessor) => { - const instantiationService = accessor.get(IInstantiationService); - instantiationService.createInstance(StopExtensionHostProfileAction, StopExtensionHostProfileAction.ID, StopExtensionHostProfileAction.LABEL).run(); -}); - -CommandsRegistry.registerCommand(SaveExtensionHostProfileAction.ID, (accessor: ServicesAccessor) => { - const instantiationService = accessor.get(IInstantiationService); - instantiationService.createInstance(SaveExtensionHostProfileAction, SaveExtensionHostProfileAction.ID, SaveExtensionHostProfileAction.LABEL).run(); -}); - -// Running extensions - -MenuRegistry.appendMenuItem(MenuId.EditorTitle, { - command: { - id: DebugExtensionHostAction.ID, - title: DebugExtensionHostAction.LABEL, - icon: Codicon.debugStart - }, - group: 'navigation', - when: ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID) -}); - -MenuRegistry.appendMenuItem(MenuId.EditorTitle, { - command: { - id: StartExtensionHostProfileAction.ID, - title: StartExtensionHostProfileAction.LABEL, - icon: Codicon.circleFilled - }, - group: 'navigation', - when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID), CONTEXT_PROFILE_SESSION_STATE.notEqualsTo('running')) -}); - -MenuRegistry.appendMenuItem(MenuId.EditorTitle, { - command: { - id: StopExtensionHostProfileAction.ID, - title: StopExtensionHostProfileAction.LABEL, - icon: Codicon.debugStop - }, - group: 'navigation', - when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID), CONTEXT_PROFILE_SESSION_STATE.isEqualTo('running')) -}); - -MenuRegistry.appendMenuItem(MenuId.EditorTitle, { - command: { - id: SaveExtensionHostProfileAction.ID, - title: SaveExtensionHostProfileAction.LABEL, - icon: Codicon.saveAll, - precondition: CONTEXT_EXTENSION_HOST_PROFILE_RECORDED - }, - group: 'navigation', - when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID)) -}); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts index 47e7af95e6..91907d3feb 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts @@ -11,14 +11,14 @@ 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 { IExtensionHostProfileService } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor'; +import { IExtensionHostProfileService } from 'vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { localize } from 'vs/nls'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { createSlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions'; +import { createSlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-sandbox/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'; diff --git a/src/vs/workbench/contrib/extensions/electron-browser/debugExtensionHostAction.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction.ts similarity index 97% rename from src/vs/workbench/contrib/extensions/electron-browser/debugExtensionHostAction.ts rename to src/vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction.ts index 03700517f9..f2846e7552 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/debugExtensionHostAction.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction.ts @@ -10,7 +10,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { IDebugService } from 'vs/workbench/contrib/debug/common/debug'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { randomPort } from 'vs/base/node/ports'; +import { randomPort } from 'vs/base/common/ports'; export class DebugExtensionHostAction extends Action { static readonly ID = 'workbench.extensions.action.debugExtensionHost'; diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts new file mode 100644 index 0000000000..96fb6130cd --- /dev/null +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts @@ -0,0 +1,127 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Registry } from 'vs/platform/registry/common/platform'; +import { MenuRegistry, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { EditorDescriptor, IEditorRegistry } from 'vs/workbench/browser/editor'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { RuntimeExtensionsEditor, StartExtensionHostProfileAction, StopExtensionHostProfileAction, CONTEXT_PROFILE_SESSION_STATE, CONTEXT_EXTENSION_HOST_PROFILE_RECORDED, SaveExtensionHostProfileAction } from 'vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor'; +import { DebugExtensionHostAction } from 'vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction'; +import { IEditorInputSerializer, IEditorInputFactoryRegistry, ActiveEditorContext, EditorExtensions } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { OpenExtensionsFolderAction } from 'vs/workbench/contrib/extensions/electron-sandbox/extensionsActions'; +import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; +import { ISharedProcessService } from 'vs/platform/ipc/electron-sandbox/services'; +import { ExtensionRecommendationNotificationServiceChannel } from 'vs/platform/extensionRecommendations/electron-sandbox/extensionRecommendationsIpc'; +import { Codicon } from 'vs/base/common/codicons'; + +// Running Extensions Editor +Registry.as(EditorExtensions.Editors).registerEditor( + EditorDescriptor.create(RuntimeExtensionsEditor, RuntimeExtensionsEditor.ID, localize('runtimeExtension', "Running Extensions")), + [new SyncDescriptor(RuntimeExtensionsInput)] +); + +class RuntimeExtensionsInputSerializer implements IEditorInputSerializer { + canSerialize(editorInput: EditorInput): boolean { + return true; + } + serialize(editorInput: EditorInput): string { + return ''; + } + deserialize(instantiationService: IInstantiationService): EditorInput { + return RuntimeExtensionsInput.instance; + } +} + +Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputSerializer(RuntimeExtensionsInput.ID, RuntimeExtensionsInputSerializer); + + +// Global actions + +class ExtensionsContributions implements IWorkbenchContribution { + + constructor( + @IExtensionRecommendationNotificationService extensionRecommendationNotificationService: IExtensionRecommendationNotificationService, + @ISharedProcessService sharedProcessService: ISharedProcessService, + ) { + sharedProcessService.registerChannel('extensionRecommendationNotification', new ExtensionRecommendationNotificationServiceChannel(extensionRecommendationNotificationService)); + registerAction2(OpenExtensionsFolderAction); + } +} + +const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); +workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, LifecyclePhase.Starting); + +// Register Commands + +CommandsRegistry.registerCommand(DebugExtensionHostAction.ID, (accessor: ServicesAccessor) => { + const instantiationService = accessor.get(IInstantiationService); + instantiationService.createInstance(DebugExtensionHostAction).run(); +}); + +CommandsRegistry.registerCommand(StartExtensionHostProfileAction.ID, (accessor: ServicesAccessor) => { + const instantiationService = accessor.get(IInstantiationService); + instantiationService.createInstance(StartExtensionHostProfileAction, StartExtensionHostProfileAction.ID, StartExtensionHostProfileAction.LABEL).run(); +}); + +CommandsRegistry.registerCommand(StopExtensionHostProfileAction.ID, (accessor: ServicesAccessor) => { + const instantiationService = accessor.get(IInstantiationService); + instantiationService.createInstance(StopExtensionHostProfileAction, StopExtensionHostProfileAction.ID, StopExtensionHostProfileAction.LABEL).run(); +}); + +CommandsRegistry.registerCommand(SaveExtensionHostProfileAction.ID, (accessor: ServicesAccessor) => { + const instantiationService = accessor.get(IInstantiationService); + instantiationService.createInstance(SaveExtensionHostProfileAction, SaveExtensionHostProfileAction.ID, SaveExtensionHostProfileAction.LABEL).run(); +}); + +// Running extensions + +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { + command: { + id: DebugExtensionHostAction.ID, + title: DebugExtensionHostAction.LABEL, + icon: Codicon.debugStart + }, + group: 'navigation', + when: ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID) +}); + +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { + command: { + id: StartExtensionHostProfileAction.ID, + title: StartExtensionHostProfileAction.LABEL, + icon: Codicon.circleFilled + }, + group: 'navigation', + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID), CONTEXT_PROFILE_SESSION_STATE.notEqualsTo('running')) +}); + +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { + command: { + id: StopExtensionHostProfileAction.ID, + title: StopExtensionHostProfileAction.LABEL, + icon: Codicon.debugStop + }, + group: 'navigation', + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID), CONTEXT_PROFILE_SESSION_STATE.isEqualTo('running')) +}); + +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { + command: { + id: SaveExtensionHostProfileAction.ID, + title: SaveExtensionHostProfileAction.LABEL, + icon: Codicon.saveAll, + precondition: CONTEXT_EXTENSION_HOST_PROFILE_RECORDED + }, + group: 'navigation', + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID)) +}); diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsActions.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsActions.ts index 95b614702c..412a944ed7 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsActions.ts @@ -4,31 +4,33 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { Action } from 'vs/base/common/actions'; import { IFileService } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { Schemas } from 'vs/base/common/network'; +import { Action2 } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ExtensionsLocalizedLabel } from 'vs/platform/extensionManagement/common/extensionManagement'; -export class OpenExtensionsFolderAction extends Action { +export class OpenExtensionsFolderAction extends Action2 { - static readonly ID = 'workbench.extensions.action.openExtensionsFolder'; - static readonly LABEL = localize('openExtensionsFolder', "Open Extensions Folder"); - - constructor( - id: string, - label: string, - @INativeHostService private readonly nativeHostService: INativeHostService, - @IFileService private readonly fileService: IFileService, - @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService - ) { - super(id, label, undefined, true); + constructor() { + super({ + id: 'workbench.extensions.action.openExtensionsFolder', + title: { value: localize('openExtensionsFolder', "Open Extensions Folder"), original: 'Open Extensions Folder' }, + category: ExtensionsLocalizedLabel, + f1: true + }); } - override async run(): Promise { - const extensionsHome = URI.file(this.environmentService.extensionsPath); - const file = await this.fileService.resolve(extensionsHome); + async run(accessor: ServicesAccessor): Promise { + const nativeHostService = accessor.get(INativeHostService); + const fileService = accessor.get(IFileService); + const environmentService = accessor.get(INativeWorkbenchEnvironmentService); + + const extensionsHome = URI.file(environmentService.extensionsPath); + const file = await fileService.resolve(extensionsHome); let itemToShow: URI; if (file.children && file.children.length > 0) { @@ -38,7 +40,7 @@ export class OpenExtensionsFolderAction extends Action { } if (itemToShow.scheme === Schemas.file) { - return this.nativeHostService.showItemInFolder(itemToShow.fsPath); + return nativeHostService.showItemInFolder(itemToShow.fsPath); } } } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsSlowActions.ts similarity index 99% rename from src/vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions.ts rename to src/vs/workbench/contrib/extensions/electron-sandbox/extensionsSlowActions.ts index bd03eac992..246a1fa177 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsSlowActions.ts @@ -152,7 +152,7 @@ class ReportExtensionSlowAction extends Action { this._dialogService.show( Severity.Info, localize('attach.title', "Did you attach the CPU-Profile?"), - [localize('ok', 'OK')], + undefined, { detail: localize('attach.msg', "This is a reminder to make sure that you have not forgotten to attach '{0}' to the issue you have just created.", path) } ); } @@ -186,7 +186,7 @@ class ShowExtensionSlowAction extends Action { this._dialogService.show( Severity.Info, localize('attach.title', "Did you attach the CPU-Profile?"), - [localize('ok', 'OK')], + undefined, { detail: localize('attach.msg2', "This is a reminder to make sure that you have not forgotten to attach '{0}' to an existing performance issue.", path) } ); } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/reportExtensionIssueAction.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/reportExtensionIssueAction.ts similarity index 100% rename from src/vs/workbench/contrib/extensions/electron-browser/reportExtensionIssueAction.ts rename to src/vs/workbench/contrib/extensions/electron-sandbox/reportExtensionIssueAction.ts diff --git a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts similarity index 95% rename from src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts rename to src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts index 575a98e2a0..a25617b3b9 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts @@ -17,9 +17,9 @@ import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/cont import { IStorageService } from 'vs/platform/storage/common/storage'; import { ILabelService } from 'vs/platform/label/common/label'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { SlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions'; +import { SlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-sandbox/extensionsSlowActions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { ReportExtensionIssueAction } from 'vs/workbench/contrib/extensions/electron-browser/reportExtensionIssueAction'; +import { ReportExtensionIssueAction } from 'vs/workbench/contrib/extensions/electron-sandbox/reportExtensionIssueAction'; import { AbstractRuntimeExtensionsEditor, IRuntimeExtension } from 'vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor'; import { VSBuffer } from 'vs/base/common/buffer'; import { URI } from 'vs/base/common/uri'; @@ -105,7 +105,15 @@ export class RuntimeExtensionsEditor extends AbstractRuntimeExtensionsEditor { } protected _createReportExtensionIssueAction(element: IRuntimeExtension): Action | null { - return this._instantiationService.createInstance(ReportExtensionIssueAction, element); + if (element.marketplaceInfo) { + return this._instantiationService.createInstance(ReportExtensionIssueAction, { + description: element.description, + marketplaceInfo: element.marketplaceInfo, + status: element.status, + unresponsiveProfile: element.unresponsiveProfile + }); + } + return null; } protected _createSaveExtensionHostProfileAction(): Action | null { 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 9b6a928d5e..75772c1e8d 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 @@ -175,6 +175,12 @@ function aGalleryExtension(name: string, properties: any = {}, galleryExtensionP return galleryExtension; } +class TestExtensionRecommendationsService extends ExtensionRecommendationsService { + protected override get workbenchRecommendationDelay() { + return 0; + } +} + suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}} skip suite let workspaceService: IWorkspaceContextService; let instantiationService: TestInstantiationService; @@ -311,7 +317,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} function testNoPromptForValidRecommendations(recommendations: string[]) { return setUpFolderWorkspace('myFolder', recommendations).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { assert.strictEqual(Object.keys(testObject.getAllRecommendationsWithReason()).length, recommendations.length); assert.ok(!prompted); @@ -321,7 +327,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} function testNoPromptOrRecommendationsForValidRecommendations(recommendations: string[]) { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); assert.ok(!prompted); return testObject.getWorkspaceRecommendations().then(() => { @@ -350,7 +356,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} test('ExtensionRecommendationsService: Prompt for valid workspace recommendations', async () => { await setUpFolderWorkspace('myFolder', mockTestData.recommendedExtensions); - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); await Event.toPromise(promptedEmitter.event); const recommendations = Object.keys(testObject.getAllRecommendationsWithReason()); @@ -379,7 +385,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} test('ExtensionRecommendationsService: No Prompt for valid workspace recommendations if ignoreRecommendations is set', () => { testConfigurationService.setUserConfiguration(ConfigurationKey, { ignoreRecommendations: true }); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { assert.ok(!prompted); }); @@ -389,7 +395,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} test('ExtensionRecommendationsService: No Prompt for valid workspace recommendations if showRecommendationsOnlyOnDemand is set', () => { testConfigurationService.setUserConfiguration(ConfigurationKey, { showRecommendationsOnlyOnDemand: true }); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { assert.ok(!prompted); }); @@ -407,7 +413,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} instantiationService.get(IStorageService).store('extensionsAssistant/ignored_recommendations', '["ms-dotnettools.csharp", "mockpublisher2.mockextension2"]', StorageScope.GLOBAL, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(!recommendations['ms-dotnettools.csharp']); // stored recommendation that has been globally ignored @@ -425,7 +431,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, ignoredRecommendations).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(!recommendations['ms-dotnettools.csharp']); // stored recommendation that has been workspace ignored @@ -447,7 +453,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} storageService.store('extensionsAssistant/ignored_recommendations', globallyIgnoredRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); await setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, workspaceIgnoredRecommendations); - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); await testObject.activationPromise; const recommendations = testObject.getAllRecommendationsWithReason(); @@ -467,7 +473,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} await setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions); const extensionIgnoredRecommendationsService = instantiationService.get(IExtensionIgnoredRecommendationsService); - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); await testObject.activationPromise; let recommendations = testObject.getAllRecommendationsWithReason(); @@ -499,7 +505,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} storageService.store('extensionsAssistant/ignored_recommendations', '["ms-vscode.vscode"]', StorageScope.GLOBAL, StorageTarget.MACHINE); await setUpFolderWorkspace('myFolder', []); - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); const extensionIgnoredRecommendationsService = instantiationService.get(IExtensionIgnoredRecommendationsService); extensionIgnoredRecommendationsService.onDidChangeGlobalIgnoredRecommendation(changeHandlerTarget); extensionIgnoredRecommendationsService.toggleGlobalIgnoredRecommendation(ignoredExtensionId, true); @@ -514,7 +520,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', []).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { const recommendations = testObject.getFileBasedRecommendations(); assert.strictEqual(recommendations.length, 2); @@ -533,7 +539,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', []).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { const recommendations = testObject.getFileBasedRecommendations(); assert.strictEqual(recommendations.length, 2); 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 183e89f9e2..c6ea56c6c7 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 @@ -176,14 +176,14 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.strictEqual(4, actual.rating); assert.strictEqual(100, actual.ratingCount); assert.strictEqual(false, actual.outdated); - assert.deepEqual(['pub.1', 'pub.2'], actual.dependencies); + assert.deepStrictEqual(['pub.1', 'pub.2'], actual.dependencies); }); }); test('test for empty installed extensions', async () => { testObject = await aWorkbenchService(); - assert.deepEqual([], testObject.local); + assert.deepStrictEqual([], testObject.local); }); test('test for installed extensions', async () => { @@ -233,7 +233,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.strictEqual(undefined, actual.rating); assert.strictEqual(undefined, actual.ratingCount); assert.strictEqual(false, actual.outdated); - assert.deepEqual(['pub.1', 'pub.2'], actual.dependencies); + assert.deepStrictEqual(['pub.1', 'pub.2'], actual.dependencies); actual = actuals[1]; assert.strictEqual(ExtensionType.System, actual.type); @@ -251,7 +251,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.strictEqual(undefined, actual.rating); assert.strictEqual(undefined, actual.ratingCount); assert.strictEqual(false, actual.outdated); - assert.deepEqual([], actual.dependencies); + assert.deepStrictEqual([], actual.dependencies); }); test('test installed extensions get syncs with gallery', async () => { @@ -327,7 +327,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.strictEqual(4, actual.rating); assert.strictEqual(100, actual.ratingCount); assert.strictEqual(true, actual.outdated); - assert.deepEqual(['pub.1'], actual.dependencies); + assert.deepStrictEqual(['pub.1'], actual.dependencies); actual = actuals[1]; assert.strictEqual(ExtensionType.System, actual.type); @@ -345,7 +345,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.strictEqual(undefined, actual.rating); assert.strictEqual(undefined, actual.ratingCount); assert.strictEqual(false, actual.outdated); - assert.deepEqual([], actual.dependencies); + assert.deepStrictEqual([], actual.dependencies); }); }); diff --git a/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts b/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts index fe529e4d90..f2ad62be46 100644 --- a/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts +++ b/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts @@ -6,7 +6,6 @@ import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { URI } from 'vs/base/common/uri'; -import { IExternalTerminalConfiguration, IExternalTerminalService } from 'vs/workbench/contrib/externalTerminal/common/externalTerminal'; import { MenuId, MenuRegistry, IMenuItem } from 'vs/platform/actions/common/actions'; import { ITerminalService as IIntegratedTerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ResourceContextKey } from 'vs/workbench/common/resources'; @@ -26,6 +25,7 @@ import { isWeb, isWindows } from 'vs/base/common/platform'; import { dirname, basename } from 'vs/base/common/path'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; +import { IExternalTerminalConfiguration, IExternalTerminalService } from 'vs/platform/externalTerminal/common/externalTerminal'; const OPEN_IN_TERMINAL_COMMAND_ID = 'openInTerminal'; CommandsRegistry.registerCommand({ @@ -44,11 +44,7 @@ CommandsRegistry.registerCommand({ // Always use integrated terminal when using a remote const useIntegratedTerminal = remoteAgentService.getConnection() || configurationService.getValue().terminal.explorerKind === 'integrated'; if (useIntegratedTerminal) { - - // TODO: Use uri for cwd in createterminal - - const opened: { [path: string]: boolean } = {}; targets.map(({ stat }) => { const resource = stat!.resource; @@ -121,8 +117,7 @@ export class ExternalTerminalContribution extends Disposable implements IWorkben this._openInTerminalMenuItem.command.title = nls.localize('scopedConsoleAction.integrated', "Open in Integrated Terminal"); return; } - - if (isWindows && config.external.windowsExec) { + if (isWindows && config.external?.windowsExec) { const file = basename(config.external.windowsExec); if (file === 'wt' || file === 'wt.exe') { this._openInTerminalMenuItem.command.title = nls.localize('scopedConsoleAction.wt', "Open in Windows Terminal"); diff --git a/src/vs/workbench/contrib/externalTerminal/node/externalTerminal.contribution.ts b/src/vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution.ts similarity index 50% rename from src/vs/workbench/contrib/externalTerminal/node/externalTerminal.contribution.ts rename to src/vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution.ts index b89af69c36..e318f59729 100644 --- a/src/vs/workbench/contrib/externalTerminal/node/externalTerminal.contribution.ts +++ b/src/vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import * as paths from 'vs/base/common/path'; -import { IExternalTerminalService } from 'vs/workbench/contrib/externalTerminal/common/externalTerminal'; +import { DEFAULT_TERMINAL_OSX, IExternalTerminalService } from 'vs/platform/externalTerminal/common/externalTerminal'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { KEYBINDING_CONTEXT_TERMINAL_NOT_FOCUSED } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -13,12 +13,10 @@ import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { Schemas } from 'vs/base/common/network'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; -import { WindowsExternalTerminalService, MacExternalTerminalService, LinuxExternalTerminalService } from 'vs/workbench/contrib/externalTerminal/node/externalTerminalService'; import { IConfigurationRegistry, Extensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { isWindows, isMacintosh, isLinux } from 'vs/base/common/platform'; -import { DEFAULT_TERMINAL_OSX } from 'vs/workbench/contrib/externalTerminal/node/externalTerminal'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IExternalTerminalMainService } from 'vs/platform/externalTerminal/electron-sandbox/externalTerminalMainService'; const OPEN_NATIVE_CONSOLE_COMMAND_ID = 'workbench.action.terminal.openNativeConsole'; KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -54,53 +52,54 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { } }); -if (isWindows) { - registerSingleton(IExternalTerminalService, WindowsExternalTerminalService, true); -} else if (isMacintosh) { - registerSingleton(IExternalTerminalService, MacExternalTerminalService, true); -} else if (isLinux) { - registerSingleton(IExternalTerminalService, LinuxExternalTerminalService, true); -} +export class ExternalTerminalContribution implements IWorkbenchContribution { -LinuxExternalTerminalService.getDefaultTerminalLinuxReady().then(defaultTerminalLinux => { - let configurationRegistry = Registry.as(Extensions.Configuration); - configurationRegistry.registerConfiguration({ - id: 'externalTerminal', - order: 100, - title: nls.localize('terminalConfigurationTitle', "External Terminal"), - type: 'object', - properties: { - 'terminal.explorerKind': { - type: 'string', - enum: [ - 'integrated', - 'external' - ], - enumDescriptions: [ - nls.localize('terminal.explorerKind.integrated', "Use VS Code's integrated terminal."), - nls.localize('terminal.explorerKind.external', "Use the configured external terminal.") - ], - description: nls.localize('explorer.openInTerminalKind', "Customizes what kind of terminal to launch."), - default: 'integrated' - }, - 'terminal.external.windowsExec': { - type: 'string', - description: nls.localize('terminal.external.windowsExec', "Customizes which terminal to run on Windows."), - default: WindowsExternalTerminalService.getDefaultTerminalWindows(), - scope: ConfigurationScope.APPLICATION - }, - 'terminal.external.osxExec': { - type: 'string', - description: nls.localize('terminal.external.osxExec', "Customizes which terminal application to run on macOS."), - default: DEFAULT_TERMINAL_OSX, - scope: ConfigurationScope.APPLICATION - }, - 'terminal.external.linuxExec': { - type: 'string', - description: nls.localize('terminal.external.linuxExec', "Customizes which terminal to run on Linux."), - default: defaultTerminalLinux, - scope: ConfigurationScope.APPLICATION + public _serviceBrand: undefined; + constructor(@IExternalTerminalMainService private readonly _externalTerminalService: IExternalTerminalMainService) { + this._updateConfiguration(); + } + + private async _updateConfiguration(): Promise { + const terminals = await this._externalTerminalService.getDefaultTerminalForPlatforms(); + let configurationRegistry = Registry.as(Extensions.Configuration); + configurationRegistry.registerConfiguration({ + id: 'externalTerminal', + order: 100, + title: nls.localize('terminalConfigurationTitle', "External Terminal"), + type: 'object', + properties: { + 'terminal.explorerKind': { + type: 'string', + enum: [ + 'integrated', + 'external' + ], + enumDescriptions: [ + nls.localize('terminal.explorerKind.integrated', "Use VS Code's integrated terminal."), + nls.localize('terminal.explorerKind.external', "Use the configured external terminal.") + ], + description: nls.localize('explorer.openInTerminalKind', "Customizes what kind of terminal to launch."), + default: 'integrated' + }, + 'terminal.external.windowsExec': { + type: 'string', + description: nls.localize('terminal.external.windowsExec', "Customizes which terminal to run on Windows."), + default: terminals.windows, + scope: ConfigurationScope.APPLICATION + }, + 'terminal.external.osxExec': { + type: 'string', + description: nls.localize('terminal.external.osxExec', "Customizes which terminal application to run on macOS."), + default: DEFAULT_TERMINAL_OSX, + scope: ConfigurationScope.APPLICATION + }, + 'terminal.external.linuxExec': { + type: 'string', + description: nls.localize('terminal.external.linuxExec', "Customizes which terminal to run on Linux."), + default: terminals.linux, + scope: ConfigurationScope.APPLICATION + } } - } - }); -}); + }); + } +} diff --git a/src/vs/workbench/contrib/feedback/browser/feedback.ts b/src/vs/workbench/contrib/feedback/browser/feedback.ts index d1f21eb557..9cdca0d684 100644 --- a/src/vs/workbench/contrib/feedback/browser/feedback.ts +++ b/src/vs/workbench/contrib/feedback/browser/feedback.ts @@ -13,7 +13,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { IIntegrityService } from 'vs/workbench/services/integrity/common/integrity'; import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { attachButtonStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; -import { editorWidgetBackground, editorWidgetForeground, widgetShadow, inputBorder, inputForeground, inputBackground, inputActiveOptionBorder, editorBackground, textLinkForeground, contrastBorder, darken } from 'vs/platform/theme/common/colorRegistry'; +import { editorWidgetBackground, editorWidgetForeground, widgetShadow, inputBorder, inputForeground, inputBackground, inputActiveOptionBorder, editorBackground, textLinkForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { IAnchor } from 'vs/base/browser/ui/contextview/contextview'; import { Button } from 'vs/base/browser/ui/button/button'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -142,7 +142,7 @@ export class FeedbackWidget extends Dropdown { if (darkenFactor) { const backgroundBaseColor = theme.getColor(editorWidgetBackground); if (backgroundBaseColor) { - const backgroundColor = darken(backgroundBaseColor, darkenFactor)(theme); + const backgroundColor = backgroundBaseColor.darken(darkenFactor); if (backgroundColor) { closeBtn.style.backgroundColor = backgroundColor.toString(); } diff --git a/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts b/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts index e7b61f0300..7ffd2caee2 100644 --- a/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts +++ b/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts @@ -19,6 +19,7 @@ import { CATEGORIES } from 'vs/workbench/common/actions'; import { assertIsDefined } from 'vs/base/common/types'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { HIDE_NOTIFICATIONS_CENTER, HIDE_NOTIFICATION_TOAST } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; +import { isIOS } from 'vs/base/common/platform'; class TwitterFeedbackService implements IFeedbackDelegate { @@ -70,7 +71,7 @@ export class FeedbackStatusbarConribution extends Disposable implements IWorkben ) { super(); - if (productService.sendASmile) { + if (productService.sendASmile && !isIOS) { this.createFeedbackStatusEntry(); this.registerListeners(); } @@ -79,7 +80,7 @@ export class FeedbackStatusbarConribution extends Disposable implements IWorkben private createFeedbackStatusEntry(): void { // Status entry - this.entry = this._register(this.statusbarService.addEntry(this.getStatusEntry(), 'status.feedback', localize('status.feedback', "Tweet Feedback"), StatusbarAlignment.RIGHT, -100 /* towards the end of the right hand side */)); + this.entry = this._register(this.statusbarService.addEntry(this.getStatusEntry(), 'status.feedback', StatusbarAlignment.RIGHT, -100 /* towards the end of the right hand side */)); // Command to toggle CommandsRegistry.registerCommand(FeedbackStatusbarConribution.TOGGLE_FEEDBACK_COMMAND, () => this.toggleFeedback()); @@ -135,6 +136,7 @@ export class FeedbackStatusbarConribution extends Disposable implements IWorkben private getStatusEntry(showBeak?: boolean): IStatusbarEntry { return { + name: localize('status.feedback.name', "Feedback"), text: '$(feedback)', ariaLabel: localize('status.feedback', "Tweet Feedback"), tooltip: localize('status.feedback', "Tweet Feedback"), diff --git a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts index a0e83af5ae..00856c79b8 100644 --- a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts @@ -7,12 +7,12 @@ import { localize } from 'vs/nls'; import { BaseBinaryResourceEditor } from 'vs/workbench/browser/parts/editor/binaryEditor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; 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 { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { BINARY_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { EditorOverride } from 'vs/platform/editor/common/editor'; +import { EditorOverride, IEditorOptions } from 'vs/platform/editor/common/editor'; import { IEditorOverrideService } from 'vs/workbench/services/editor/common/editorOverrideService'; /** @@ -40,7 +40,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { ); } - private async openInternal(input: EditorInput, options: EditorOptions | undefined): Promise { + private async openInternal(input: EditorInput, options: IEditorOptions | undefined): Promise { if (input instanceof FileEditorInput && this.group) { // Enforce to open the input as text to enable our text based viewer @@ -49,13 +49,14 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { // Try to let the user pick an override if there is one availabe const overridenInput = await this.editorOverrideService.resolveEditorOverride(input, { ...options, override: EditorOverride.PICK, }, this.group); - let newOptions = overridenInput?.options ?? options; - newOptions = { ...newOptions, override: EditorOverride.DISABLED }; // Replace the overrriden input, with the text based input await this.editorService.replaceEditors([{ editor: input, replacement: overridenInput?.editor ?? input, - options: newOptions, + options: { + ...overridenInput?.options ?? options, + override: EditorOverride.DISABLED + } }], overridenInput?.group ?? this.group); } } diff --git a/src/vs/workbench/contrib/files/browser/editors/fileEditorHandler.ts b/src/vs/workbench/contrib/files/browser/editors/fileEditorHandler.ts new file mode 100644 index 0000000000..e4a0bd1002 --- /dev/null +++ b/src/vs/workbench/contrib/files/browser/editors/fileEditorHandler.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { URI, UriComponents } from 'vs/base/common/uri'; +import { IEditorInputSerializer } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { isEqual } from 'vs/base/common/resources'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; +import { IFileService } from 'vs/platform/files/common/files'; + +interface ISerializedFileEditorInput { + resourceJSON: UriComponents; + preferredResourceJSON?: UriComponents; + name?: string; + description?: string; + encoding?: string; + modeId?: string; +} + +export class FileEditorInputSerializer implements IEditorInputSerializer { + + canSerialize(editorInput: EditorInput): boolean { + return true; + } + + serialize(editorInput: EditorInput): string { + const fileEditorInput = editorInput as FileEditorInput; + 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 class FileEditorWorkingCopyEditorHandler extends Disposable implements IWorkbenchContribution { + + constructor( + @IWorkingCopyEditorService private readonly workingCopyEditorService: IWorkingCopyEditorService, + @IEditorService private readonly editorService: IEditorService, + @IFileService private readonly fileService: IFileService + ) { + super(); + + this.installHandler(); + } + + private installHandler(): void { + this._register(this.workingCopyEditorService.registerHandler({ + handles: workingCopy => workingCopy.typeId === NO_TYPE_ID && this.fileService.canHandleResource(workingCopy.resource), + // Naturally it would make sense here to check for `instanceof FileEditorInput` + // but because some custom editors also leverage text file based working copies + // we need to do a weaker check by only comparing for the resource + isOpen: (workingCopy, editor) => isEqual(workingCopy.resource, editor.resource), + createEditor: workingCopy => this.editorService.createEditorInput({ resource: workingCopy.resource, forceFile: true }) + })); + } +} diff --git a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts similarity index 72% rename from src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts rename to src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts index f637b47cdd..4d203d8913 100644 --- a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts @@ -3,25 +3,25 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; -import { IFileEditorInput, Verbosity, GroupIdentifier, IMoveResult, isTextEditorPane } from 'vs/workbench/common/editor'; +import { IFileEditorInput, Verbosity, GroupIdentifier, IMoveResult, EditorInputCapabilities, IEditorDescriptor, IEditorPane } from 'vs/workbench/common/editor'; import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; +import { EditorOverride, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; -import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { FileOperationError, FileOperationResult, FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files'; import { ITextFileService, TextFileEditorModelState, TextFileResolveReason, TextFileOperationError, TextFileOperationResult, ITextFileEditorModel, EncodingMode } from 'vs/workbench/services/textfile/common/textfiles'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IReference, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { FILE_EDITOR_INPUT_ID, TEXT_FILE_EDITOR_ID, BINARY_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; -import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { isEqual } from 'vs/base/common/resources'; import { Event } from 'vs/base/common/event'; -import { IEditorViewState } from 'vs/editor/common/editorCommon'; import { Schemas } from 'vs/base/common/network'; +import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; const enum ForceOpenAs { None, @@ -38,10 +38,31 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements return FILE_EDITOR_INPUT_ID; } + override get capabilities(): EditorInputCapabilities { + let capabilities = EditorInputCapabilities.None; + + if (this.model) { + if (this.model.isReadonly()) { + capabilities |= EditorInputCapabilities.Readonly; + } + } else { + if (this.fileService.canHandleResource(this.resource)) { + if (this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly)) { + capabilities |= EditorInputCapabilities.Readonly; + } + } else { + capabilities |= EditorInputCapabilities.Untitled; + } + } + + return capabilities; + } + private preferredName: string | undefined; private preferredDescription: string | undefined; private preferredEncoding: string | undefined; private preferredMode: string | undefined; + private preferredContents: string | undefined; private forceOpenAs: ForceOpenAs = ForceOpenAs.None; @@ -57,16 +78,17 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements preferredDescription: string | undefined, preferredEncoding: string | undefined, preferredMode: string | undefined, + preferredContents: string | undefined, @IInstantiationService private readonly instantiationService: IInstantiationService, @ITextFileService textFileService: ITextFileService, @ITextModelService private readonly textModelResolverService: ITextModelService, @ILabelService labelService: ILabelService, @IFileService fileService: IFileService, - @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, @IEditorService editorService: IEditorService, - @IEditorGroupsService editorGroupService: IEditorGroupsService + @IPathService private readonly pathService: IPathService ) { - super(resource, preferredResource, editorService, editorGroupService, textFileService, labelService, fileService, filesConfigurationService); + super(resource, preferredResource, editorService, textFileService, labelService, fileService); this.model = this.textFileService.files.get(resource); @@ -86,19 +108,19 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements this.setPreferredMode(preferredMode); } + if (typeof preferredContents === 'string') { + this.setPreferredContents(preferredContents); + } + + // Attach to model that matches our resource once created + this._register(this.textFileService.files.onDidCreate(model => this.onDidCreateTextFileModel(model))); + // If a file model already exists, make sure to wire it in if (this.model) { this.registerModelListeners(this.model); } } - protected override registerListeners(): void { - super.registerListeners(); - - // Attach to model that matches our resource once created - this._register(this.textFileService.files.onDidCreate(model => this.onDidCreateTextFileModel(model))); - } - private onDidCreateTextFileModel(model: ITextFileEditorModel): void { // Once the text file model is created, we keep it inside @@ -118,6 +140,7 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements // re-emit some events from the model this.modelListeners.add(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); this.modelListeners.add(model.onDidChangeOrphaned(() => this._onDidChangeLabel.fire())); + this.modelListeners.add(model.onDidChangeReadonly(() => this._onDidChangeCapabilities.fire())); // important: treat save errors as potential dirty change because // a file that is in save conflict or error will report dirty even @@ -132,12 +155,12 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements } override getName(): string { - return this.preferredName || this.decorateLabel(super.getName()); + return this.preferredName || super.getName(); } setPreferredName(name: string): void { if (!this.allowLabelOverride()) { - return; // block for specific schemes we own + return; // block for specific schemes we consider to be owning } if (this.preferredName !== name) { @@ -148,7 +171,10 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements } private allowLabelOverride(): boolean { - return this.resource.scheme !== Schemas.file && this.resource.scheme !== Schemas.vscodeRemote && this.resource.scheme !== Schemas.userData; + return this.resource.scheme !== this.pathService.defaultUriScheme && + this.resource.scheme !== Schemas.userData && + this.resource.scheme !== Schemas.file && + this.resource.scheme !== Schemas.vscodeRemote; } getPreferredName(): string | undefined { @@ -161,7 +187,7 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements setPreferredDescription(description: string): void { if (!this.allowLabelOverride()) { - return; // block for specific schemes we own + return; // block for specific schemes we consider to be owning } if (this.preferredDescription !== description) { @@ -175,22 +201,6 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements return this.preferredDescription; } - override getTitle(verbosity: Verbosity): string { - switch (verbosity) { - case Verbosity.SHORT: - return this.decorateLabel(super.getName()); - case Verbosity.MEDIUM: - case Verbosity.LONG: - return this.decorateLabel(super.getTitle(verbosity)); - } - } - - private decorateLabel(label: string): string { - const orphaned = this.model?.hasState(TextFileEditorModelState.ORPHAN); - const readonly = this.isReadonly(); - return decorateFileEditorLabel(label, { orphaned: !!orphaned, readonly }); - } - getEncoding(): string | undefined { if (this.model) { return this.model.getEncoding(); @@ -216,6 +226,14 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements this.setForceOpenAsText(); } + getMode(): string | undefined { + if (this.model) { + return this.model.getMode(); + } + + return this.preferredMode; + } + getPreferredMode(): string | undefined { return this.preferredMode; } @@ -233,6 +251,13 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements this.setForceOpenAsText(); } + setPreferredContents(contents: string): void { + this.preferredContents = contents; + + // contents is a good hint to open the file as text + this.setForceOpenAsText(); + } + setForceOpenAsText(): void { this.forceOpenAs = ForceOpenAs.Text; } @@ -245,12 +270,12 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements return !!(this.model?.isDirty()); } - override isReadonly(): boolean { + override isOrphaned(): boolean { if (this.model) { - return this.model.isReadonly(); + return this.model.hasState(TextFileEditorModelState.ORPHAN); } - return super.isReadonly(); + return super.isOrphaned(); } override isSaving(): boolean { @@ -263,11 +288,19 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements // and it could result in bad UX where an editor can be closed even though // it shows up as dirty and has not finished saving yet. + if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) { + return true; // a short auto save is configured, treat this as being saved + } + return super.isSaving(); } - override getPreferredEditorId(candidates: string[]): string { - return this.forceOpenAs === ForceOpenAs.Binary ? BINARY_FILE_EDITOR_ID : TEXT_FILE_EDITOR_ID; + override prefersEditor>(editors: T[]): T | undefined { + if (this.forceOpenAs === ForceOpenAs.Binary) { + return editors.find(editor => editor.typeId === BINARY_FILE_EDITOR_ID); + } + + return editors.find(editor => editor.typeId === TEXT_FILE_EDITOR_ID); } override resolve(): Promise { @@ -284,11 +317,18 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements private async doResolveAsText(): Promise { try { + // Unset preferred contents after having applied it once + // to prevent this property to stick. We still want future + // `resolve` calls to fetch the contents from disk. + const preferredContents = this.preferredContents; + this.preferredContents = undefined; + // Resolve resource via text file service and only allow // to open binary files if we are instructed so await this.textFileService.files.resolve(this.resource, { mode: this.preferredMode, encoding: this.preferredEncoding, + contents: typeof preferredContents === 'string' ? createTextBufferFactory(preferredContents) : undefined, reload: { async: true }, // trigger a reload of the model if it exists already but do not wait to show the model allowBinary: this.forceOpenAs === ForceOpenAs.Text, reason: TextFileResolveReason.EDITOR @@ -350,20 +390,29 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements }; } - private getViewStateFor(group: GroupIdentifier): IEditorViewState | undefined { - for (const editorPane of this.editorService.visibleEditorPanes) { - if (editorPane.group.id === group && this.matches(editorPane.input)) { - if (isTextEditorPane(editorPane)) { - return editorPane.getViewState(); + override asResourceEditorInput(group: GroupIdentifier): ITextResourceEditorInput { + return { + resource: this.preferredResource, + forceFile: true, + encoding: this.getEncoding(), + mode: this.getMode(), + contents: (() => { + const model = this.textFileService.files.get(this.resource); + if (model && model.isDirty()) { + return model.textEditorModel.getValue(); // only if dirty } - } - } - return undefined; + return undefined; + })(), + options: { + viewState: this.getViewStateFor(group), + override: EditorOverride.DISABLED + } + }; } override matches(otherInput: unknown): boolean { - if (otherInput === this) { + if (super.matches(otherInput)) { return true; } @@ -390,19 +439,3 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements this.cachedTextFileModelReference = undefined; } } - -export function decorateFileEditorLabel(label: string, state: { orphaned: boolean, readonly: boolean }): string { - if (state.orphaned && state.readonly) { - return localize('orphanedReadonlyFile', "{0} (deleted, read-only)", label); - } - - if (state.orphaned) { - return localize('orphanedFile', "{0} (deleted)", label); - } - - if (state.readonly) { - return localize('readonlyFile', "{0} (read-only)", label); - } - - return label; -} diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index 930d9991f1..07899a8c23 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -5,16 +5,17 @@ import { localize } from 'vs/nls'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -import { isFunction, assertIsDefined } from 'vs/base/common/types'; +import { assertIsDefined } from 'vs/base/common/types'; import { isValidBasename } from 'vs/base/common/extpath'; import { basename } from 'vs/base/common/resources'; 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'; -import { EditorOptions, TextEditorOptions, IEditorInput, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorInput, IEditorOpenContext, EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { FileOperationError, FileOperationResult, FileChangesEvent, IFileService, FileOperationEvent, FileOperation } from 'vs/platform/files/common/files'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -28,9 +29,10 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { createErrorWithActions } from 'vs/base/common/errors'; -import { EditorActivation, EditorOverride, IEditorOptions } from 'vs/platform/editor/common/editor'; +import { EditorActivation, EditorOverride, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; +import { MutableDisposable } from 'vs/base/common/lifecycle'; /** * An implementation of editor for file system resources. @@ -39,6 +41,8 @@ export class TextFileEditor extends BaseTextEditor { static readonly ID = TEXT_FILE_EDITOR_ID; + private readonly inputListener = this._register(new MutableDisposable()); + constructor( @ITelemetryService telemetryService: ITelemetryService, @IFileService private readonly fileService: IFileService, @@ -63,8 +67,8 @@ export class TextFileEditor extends BaseTextEditor { this._register(this.fileService.onDidRunOperation(e => this.onDidRunOperation(e))); // Listen to file system provider changes - this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidFileSystemProviderChange(e.scheme))); - this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidFileSystemProviderChange(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidChangeFileSystemProvider(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidChangeFileSystemProvider(e.scheme))); } private onDidFilesChange(e: FileChangesEvent): void { @@ -80,11 +84,22 @@ export class TextFileEditor extends BaseTextEditor { } } - private onDidFileSystemProviderChange(scheme: string): void { + private onDidChangeFileSystemProvider(scheme: string): void { + if (this.input?.resource.scheme === scheme) { + this.updateReadonly(this.input); + } + } + + private onDidChangeInputCapabilities(input: FileEditorInput): void { + if (this.input === input) { + this.updateReadonly(input); + } + } + + private updateReadonly(input: FileEditorInput): void { const control = this.getControl(); - const input = this.input; - if (control && input?.resource.scheme === scheme) { - control.updateOptions({ readOnly: input.isReadonly() }); + if (control) { + control.updateOptions({ readOnly: input.hasCapability(EditorInputCapabilities.Readonly) }); } } @@ -104,7 +119,10 @@ export class TextFileEditor extends BaseTextEditor { return this._input as FileEditorInput; } - override async setInput(input: FileEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override async setInput(input: FileEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + + // Update our listener for input capabilities + this.inputListener.value = input.onDidChangeCapabilities(() => this.onDidChangeInputCapabilities(input)); // Update/clear view settings if input changes this.doSaveOrClearTextEditorViewState(this.input); @@ -140,9 +158,9 @@ export class TextFileEditor extends BaseTextEditor { } } - // TextOptions (avoiding instanceof here for a reason, do not change!) - if (options && isFunction((options).apply)) { - (options).apply(textEditor, ScrollType.Immediate); + // Apply options to editor if any + if (options) { + applyTextEditorOptions(options, textEditor, ScrollType.Immediate); } // Since the resolved model provides information about being readonly @@ -156,7 +174,7 @@ export class TextFileEditor extends BaseTextEditor { } } - protected handleSetInputError(error: Error, input: FileEditorInput, options: EditorOptions | undefined): void { + protected handleSetInputError(error: Error, input: FileEditorInput, options: ITextEditorOptions | undefined): void { // In case we tried to open a file inside the text editor and the response // indicates that this is not a text file, reopen the file through the binary @@ -196,21 +214,18 @@ export class TextFileEditor extends BaseTextEditor { throw error; } - private openAsBinary(input: FileEditorInput, options: EditorOptions | undefined): void { + private openAsBinary(input: FileEditorInput, options: ITextEditorOptions | undefined): void { input.setForceOpenAsBinary(); - // Make sure to not steal away the currently active group - // because we are triggering another openEditor() call - // and do not control the initial intent that resulted - // in us now opening as binary. - const preservingOptions: IEditorOptions = { activation: EditorActivation.PRESERVE, override: EditorOverride.DISABLED }; - if (options) { - options.overwrite(preservingOptions); - } else { - options = EditorOptions.create(preservingOptions); - } - - this.editorService.openEditor(input, options, this.group); + this.editorService.openEditor(input, { + ...options, + // Make sure to not steal away the currently active group + // because we are triggering another openEditor() call + // and do not control the initial intent that resulted + // in us now opening as binary. + activation: EditorActivation.PRESERVE, + override: EditorOverride.DISABLED + }, this.group); } private async openAsFolder(input: FileEditorInput): Promise { @@ -231,6 +246,9 @@ export class TextFileEditor extends BaseTextEditor { override clearInput(): void { + // Clear input listener + this.inputListener.clear(); + // Update/clear editor view state in settings this.doSaveOrClearTextEditorViewState(this.input); diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditorTracker.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditorTracker.ts index 10b978dc3e..9834855ca5 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditorTracker.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditorTracker.ts @@ -17,6 +17,7 @@ import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/ import { FILE_EDITOR_INPUT_ID } from 'vs/workbench/contrib/files/common/files'; import { Schemas } from 'vs/base/common/network'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; +import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; export class TextFileEditorTracker extends Disposable implements IWorkbenchContribution { @@ -26,7 +27,8 @@ export class TextFileEditorTracker extends Disposable implements IWorkbenchContr @ILifecycleService private readonly lifecycleService: ILifecycleService, @IHostService private readonly hostService: IHostService, @ICodeEditorService private readonly codeEditorService: ICodeEditorService, - @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, + @IWorkingCopyEditorService private readonly workingCopyEditorService: IWorkingCopyEditorService ) { super(); @@ -57,19 +59,24 @@ export class TextFileEditorTracker extends Disposable implements IWorkbenchContr return false; // resource must be dirty } - const model = this.textFileService.files.get(resource); - if (model?.hasState(TextFileEditorModelState.PENDING_SAVE)) { + const fileModel = this.textFileService.files.get(resource); + if (fileModel?.hasState(TextFileEditorModelState.PENDING_SAVE)) { return false; // resource must not be pending to save } - if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY && !model?.hasState(TextFileEditorModelState.ERROR)) { + if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY && !fileModel?.hasState(TextFileEditorModelState.ERROR)) { // leave models auto saved after short delay unless // the save resulted in an error return false; } if (this.editorService.isOpened({ resource, typeId: resource.scheme === Schemas.untitled ? UntitledTextEditorInput.ID : FILE_EDITOR_INPUT_ID })) { - return false; // model must not be opened already as file + return false; // model must not be opened already as file (fast check via editor type) + } + + const model = fileModel ?? this.textFileService.untitled.get(resource); + if (model && this.workingCopyEditorService.findEditor(model)) { + return false; // model must not be opened already as file (slower check via working copy) } return true; diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts index 46c8b4cd0f..7ab159cfab 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts @@ -18,7 +18,7 @@ import { ResourceMap } from 'vs/base/common/map'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { TextFileContentProvider } from 'vs/workbench/contrib/files/common/files'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { SAVE_FILE_AS_LABEL } from 'vs/workbench/contrib/files/browser/fileCommands'; import { INotificationService, INotificationHandle, INotificationActions, Severity } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; diff --git a/src/vs/workbench/contrib/files/browser/explorerService.ts b/src/vs/workbench/contrib/files/browser/explorerService.ts index 1c025ab92e..b0730762d2 100644 --- a/src/vs/workbench/contrib/files/browser/explorerService.ts +++ b/src/vs/workbench/contrib/files/browser/explorerService.ts @@ -6,7 +6,7 @@ import { Event } from 'vs/base/common/event'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { IFilesConfiguration, SortOrder } from 'vs/workbench/contrib/files/common/files'; +import { IFilesConfiguration, ISortOrderConfiguration, SortOrder, LexicographicOptions } from 'vs/workbench/contrib/files/common/files'; 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'; @@ -33,6 +33,7 @@ export class ExplorerService implements IExplorerService { private readonly disposables = new DisposableStore(); private editable: { stat: ExplorerItem, data: IEditableData } | undefined; private _sortOrder: SortOrder; + private _lexicographicOptions: LexicographicOptions; private cutItems: ExplorerItem[] | undefined; private view: IExplorerView | undefined; private model: ExplorerModel; @@ -50,6 +51,7 @@ export class ExplorerService implements IExplorerService { @IProgressService private readonly progressService: IProgressService ) { this._sortOrder = this.configurationService.getValue('explorer.sortOrder'); + this._lexicographicOptions = this.configurationService.getValue('explorer.sortOrderLexicographicOptions'); this.model = new ExplorerModel(this.contextService, this.uriIdentityService, this.fileService); this.disposables.add(this.model); @@ -125,8 +127,11 @@ export class ExplorerService implements IExplorerService { return this.model.roots; } - get sortOrder(): SortOrder { - return this._sortOrder; + get sortOrderConfiguration(): ISortOrderConfiguration { + return { + sortOrder: this._sortOrder, + lexicographicOptions: this._lexicographicOptions, + }; } registerView(contextProvider: IExplorerView): void { @@ -226,7 +231,7 @@ export class ExplorerService implements IExplorerService { } // Stat needs to be resolved first and then revealed - const options: IResolveFileOptions = { resolveTo: [resource], resolveMetadata: this.sortOrder === SortOrder.Modified }; + const options: IResolveFileOptions = { resolveTo: [resource], resolveMetadata: this._sortOrder === SortOrder.Modified }; const root = this.findClosestRoot(resource); if (!root) { return undefined; @@ -278,7 +283,7 @@ export class ExplorerService implements IExplorerService { // Add the new file to its parent (Model) await Promise.all(parents.map(async p => { // We have to check if the parent is resolved #29177 - const resolveMetadata = this.sortOrder === `modified`; + const resolveMetadata = this._sortOrder === `modified`; if (!p.isDirectoryResolved) { const stat = await this.fileService.resolve(p.resource, { resolveMetadata }); if (stat) { @@ -348,13 +353,22 @@ export class ExplorerService implements IExplorerService { } private async onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): Promise { - const configSortOrder = configuration?.explorer?.sortOrder || 'default'; + let shouldRefresh = false; + + const configSortOrder = configuration?.explorer?.sortOrder || SortOrder.Default; if (this._sortOrder !== configSortOrder) { - const shouldRefresh = this._sortOrder !== undefined; - this._sortOrder = configSortOrder as SortOrder; // {{SQL CARBON EDIT}} strict-null-checks - if (shouldRefresh) { - await this.refresh(); - } + shouldRefresh = this._sortOrder !== undefined; + this._sortOrder = configSortOrder as SortOrder; // {{SQL CARBON EDIT}} strict-null-checks; + } + + const configLexicographicOptions = configuration?.explorer?.sortOrderLexicographicOptions || LexicographicOptions.Default; + if (this._lexicographicOptions !== configLexicographicOptions) { + shouldRefresh = shouldRefresh || this._lexicographicOptions !== undefined; + this._lexicographicOptions = configLexicographicOptions; + } + + if (shouldRefresh) { + await this.refresh(); } } diff --git a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts index 36c51a8e14..f2a3558c5e 100644 --- a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts +++ b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts @@ -37,7 +37,7 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { WorkbenchStateContext, RemoteNameContext } from 'vs/workbench/browser/contextkeys'; import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; import { AddRootFolderAction, OpenFolderAction, OpenFileFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; -import { isMacintosh } from 'vs/base/common/platform'; +import { isMacintosh, isWeb } from 'vs/base/common/platform'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; @@ -294,7 +294,7 @@ viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { order: 1 }); -const commandId = isMacintosh ? OpenFileFolderAction.ID : OpenFolderAction.ID; +const commandId = (isMacintosh && !isWeb) ? OpenFileFolderAction.ID : OpenFolderAction.ID; viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { content: localize({ key: 'remoteNoFolderHelp', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, "Connected to remote.\n[Open Folder](command:{0})", commandId), diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 4e0aeac1b9..daee2a4c5e 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -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 { 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, UPLOAD_COMMAND_ID, UPLOAD_LABEL } 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'; @@ -113,7 +113,7 @@ const CUT_FILE_ID = 'filesExplorer.cut'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: CUT_FILE_ID, weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated()), + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext), primary: KeyMod.CtrlCmd | KeyCode.KEY_X, handler: cutFileHandler, }); @@ -223,7 +223,7 @@ appendToCommandPalette(COMPARE_WITH_SAVED_COMMAND_ID, { value: nls.localize('com appendToCommandPalette(SAVE_FILE_AS_COMMAND_ID, { value: SAVE_FILE_AS_LABEL, original: 'Save As...' }, category); appendToCommandPalette(NEW_FILE_COMMAND_ID, { value: NEW_FILE_LABEL, original: 'New File' }, category, WorkspaceFolderCountContext.notEqualsTo('0')); appendToCommandPalette(NEW_FOLDER_COMMAND_ID, { value: NEW_FOLDER_LABEL, original: 'New Folder' }, category, WorkspaceFolderCountContext.notEqualsTo('0')); -appendToCommandPalette(DOWNLOAD_COMMAND_ID, { value: DOWNLOAD_LABEL, original: 'Download' }, category, ContextKeyExpr.and(ResourceContextKey.Scheme.notEqualsTo(Schemas.file))); +appendToCommandPalette(DOWNLOAD_COMMAND_ID, { value: DOWNLOAD_LABEL, original: 'Download...' }, category, ContextKeyExpr.and(ResourceContextKey.Scheme.notEqualsTo(Schemas.file))); appendToCommandPalette(NEW_UNTITLED_FILE_COMMAND_ID, { value: locConstants.fileActionsContributionNewQuery, original: 'New Query' }, category); // {{SQL CARBON EDIT}} New Query label for normal untitled file appendToCommandPalette(NEW_UNTITLED_PLAIN_FILE_COMMAND_ID, { value: NEW_UNTITLED_FILE_LABEL, original: 'New File' }, category); // {{SQL CARBON EDIT}} New File label for untitled plain file @@ -462,7 +462,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { id: CUT_FILE_ID, title: nls.localize('cut', "Cut") }, - when: ExplorerRootContext.toNegated() + when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext) }); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { @@ -487,8 +487,8 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { }); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, ({ - group: '5_cutcopypaste', - order: 30, + group: '5b_importexport', + order: 10, command: { id: DOWNLOAD_COMMAND_ID, title: DOWNLOAD_LABEL, @@ -503,16 +503,33 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, ({ ) })); +MenuRegistry.appendMenuItem(MenuId.ExplorerContext, ({ + group: '5b_importexport', + order: 20, + command: { + id: UPLOAD_COMMAND_ID, + title: UPLOAD_LABEL, + }, + when: ContextKeyExpr.and( + // only in web + IsWebContext, + // only on folders + ExplorerFolderContext, + // only on editable folders + ExplorerResourceNotReadonlyContext + ) +})); + MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { group: '6_copypath', - order: 30, + order: 10, command: copyPathCommand, when: ResourceContextKey.IsFileSystemResource }); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { group: '6_copypath', - order: 30, + order: 20, command: copyRelativePathCommand, when: ResourceContextKey.IsFileSystemResource }); diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 1562aab0b0..71c6ce02a8 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -4,16 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { isWindows, isWeb } from 'vs/base/common/platform'; +import { isWindows } from 'vs/base/common/platform'; import * as extpath from 'vs/base/common/extpath'; import { extname, basename } from 'vs/base/common/path'; import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Action } from 'vs/base/common/actions'; -import { DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { VIEWLET_ID, IFilesConfiguration, VIEW_ID } from 'vs/workbench/contrib/files/common/files'; -import { ByteSize, IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; +import { IFileService } from 'vs/platform/files/common/files'; import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { IQuickInputService, ItemActivation } from 'vs/platform/quickinput/common/quickInput'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -28,8 +28,8 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { FileAccess, Schemas } from 'vs/base/common/network'; -import { IDialogService, IConfirmationResult, getFileNamesMessage, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { Schemas } from 'vs/base/common/network'; +import { IDialogService, IConfirmationResult, getFileNamesMessage } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Constants } from 'vs/base/common/uint'; @@ -37,26 +37,19 @@ import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/e import { coalesce } from 'vs/base/common/arrays'; import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; import { getErrorMessage } from 'vs/base/common/errors'; -import { WebFileSystemAccess, triggerDownload } from 'vs/base/browser/dom'; -import { mnemonicButtonLabel } from 'vs/base/common/labels'; +import { triggerUpload } from 'vs/base/browser/dom'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy'; -import { RunOnceWorker, sequence, timeout } from 'vs/base/common/async'; +import { timeout } from 'vs/base/common/async'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; -import { once } from 'vs/base/common/functional'; import { Codicon } from 'vs/base/common/codicons'; import { IViewsService } from 'vs/workbench/common/views'; import { trim, rtrim } from 'vs/base/common/strings'; -import { IProgressService, IProgressStep, ProgressLocation } from 'vs/platform/progress/common/progress'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { ILogService } from 'vs/platform/log/common/log'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; -import { listenStream } from 'vs/base/common/stream'; -import { EditorOverride } from 'vs/platform/editor/common/editor'; -import { ContributedEditorPriority, IEditorOverrideService } from 'vs/workbench/services/editor/common/editorOverrideService'; +import { BrowserFileUpload, FileDownload } from 'vs/workbench/contrib/files/browser/fileImportExport'; export const NEW_FILE_COMMAND_ID = 'explorer.newFile'; export const NEW_FILE_LABEL = nls.localize('newFile', "New File"); @@ -67,7 +60,10 @@ export const MOVE_FILE_TO_TRASH_LABEL = nls.localize('delete', "Delete"); export const COPY_FILE_LABEL = nls.localize('copyFile', "Copy"); export const PASTE_FILE_LABEL = nls.localize('pasteFile', "Paste"); export const FileCopiedContext = new RawContextKey('fileCopied', false); +export const DOWNLOAD_COMMAND_ID = 'explorer.download'; export const DOWNLOAD_LABEL = nls.localize('download', "Download..."); +export const UPLOAD_COMMAND_ID = 'explorer.upload'; +export const UPLOAD_LABEL = nls.localize('upload', "Upload..."); const CONFIRM_DELETE_SETTING_KEY = 'explorer.confirmDelete'; const MAX_UNDO_FILE_SIZE = 5000000; // 5mb @@ -444,8 +440,7 @@ export class GlobalCompareResourcesAction extends Action { label: string, @IQuickInputService private readonly quickInputService: IQuickInputService, @IEditorService private readonly editorService: IEditorService, - @ITextModelService private readonly textModelService: ITextModelService, - @IEditorOverrideService private readonly editorOverrideService: IEditorOverrideService + @ITextModelService private readonly textModelService: ITextModelService ) { super(id, label); } @@ -454,49 +449,17 @@ export class GlobalCompareResourcesAction extends Action { const activeInput = this.editorService.activeEditor; const activeResource = EditorResourceAccessor.getOriginalUri(activeInput); if (activeResource && this.textModelService.canHandleResource(activeResource)) { - - // Define a one-time override that has highest priority - // and matches every resource to be able to create a - // diff editor to show the comparison. - const editorOverrideDisposable = this.editorOverrideService.registerContributionPoint('*', { - id: GlobalCompareResourcesAction.ID, - label: GlobalCompareResourcesAction.LABEL, - priority: ContributedEditorPriority.exclusive, - detail: '', - describes: () => false - }, {}, resource => { - - // Only once! - editorOverrideDisposable.dispose(); - - // Open editor as diff if the selected editor resource - // can be handled by the text model service - if (this.textModelService.canHandleResource(resource)) { - return { - editor: this.editorService.createEditorInput({ - leftResource: activeResource, - rightResource: resource, - options: { override: EditorOverride.DISABLED, pinned: true } - }) - }; + const picks = await this.quickInputService.quickAccess.pick('', { itemActivation: ItemActivation.SECOND }); + if (picks?.length === 1) { + const resource = (picks[0] as unknown as { resource: unknown }).resource; + if (URI.isUri(resource) && this.textModelService.canHandleResource(resource)) { + this.editorService.openEditor({ + originalInput: { resource: activeResource }, + modifiedInput: { resource: resource }, + options: { pinned: true } + }); } - - // Otherwise stay on current resource - return { - editor: this.editorService.createEditorInput({ - resource: activeResource, - options: { override: EditorOverride.DISABLED, pinned: true } - }) - }; - }); - - once(this.quickInputService.onHide)((async () => { - await timeout(0); // prevent race condition with editor - editorOverrideDisposable.dispose(); - })); - - // Bring up quick access - this.quickInputService.quickAccess.show('', { itemActivation: ItemActivation.SECOND }); + } } } } @@ -639,7 +602,7 @@ export class ShowOpenedFileInNewWindow extends Action { label: string, @IEditorService private readonly editorService: IEditorService, @IHostService private readonly hostService: IHostService, - @INotificationService private readonly notificationService: INotificationService, + @IDialogService private readonly dialogService: IDialogService, @IFileService private readonly fileService: IFileService ) { super(id, label); @@ -651,7 +614,7 @@ export class ShowOpenedFileInNewWindow extends Action { if (this.fileService.canHandleResource(fileResource)) { this.hostService.openWindow([{ fileUri: fileResource }], { forceNewWindow: true }); } else { - this.notificationService.info(nls.localize('openFileToShowInNewWindow.unsupportedschema', "The active editor must contain an openable resource.")); + this.dialogService.show(Severity.Error, nls.localize('openFileToShowInNewWindow.unsupportedschema', "The active editor must contain an openable resource.")); } } } @@ -718,7 +681,7 @@ function trimLongName(name: string): string { return name; } -export function getWellFormedFileName(filename: string): string { +function getWellFormedFileName(filename: string): string { if (!filename) { return filename; } @@ -726,8 +689,7 @@ export function getWellFormedFileName(filename: string): string { // Trim tabs filename = trim(filename, '\t'); - // Remove trailing dots and slashes - filename = rtrim(filename, '.'); + // Remove trailing slashes filename = rtrim(filename, '/'); filename = rtrim(filename, '\\'); @@ -768,8 +730,9 @@ export class CompareWithClipboardAction extends Action { const editorLabel = nls.localize('clipboardComparisonLabel', "Clipboard ↔ {0}", name); await this.editorService.openEditor({ - leftResource: resource.with({ scheme }), - rightResource: resource, label: editorLabel, + originalInput: { resource: resource.with({ scheme }) }, + modifiedInput: { resource: resource }, + label: editorLabel, options: { pinned: true } }).finally(() => { dispose(this.registrationDisposal); @@ -965,240 +928,15 @@ export const cutFileHandler = async (accessor: ServicesAccessor) => { } }; -export const DOWNLOAD_COMMAND_ID = 'explorer.download'; const downloadFileHandler = async (accessor: ServicesAccessor) => { - const logService = accessor.get(ILogService); - const fileService = accessor.get(IFileService); - const fileDialogService = accessor.get(IFileDialogService); const explorerService = accessor.get(IExplorerService); - const progressService = accessor.get(IProgressService); + const instantiationService = accessor.get(IInstantiationService); const context = explorerService.getContext(true); const explorerItems = context.length ? context : explorerService.roots; - const cts = new CancellationTokenSource(); - - await progressService.withProgress({ - location: ProgressLocation.Window, - delay: 800, - cancellable: isWeb, - title: nls.localize('downloadingFiles', "Downloading") - }, async progress => { - return sequence(explorerItems.map(explorerItem => async () => { - if (cts.token.isCancellationRequested) { - return; - } - - // Web: use DOM APIs to download files with optional support - // for folders and large files - if (isWeb) { - const stat = await fileService.resolve(explorerItem.resource, { resolveMetadata: true }); - - if (cts.token.isCancellationRequested) { - return; - } - - const maxBlobDownloadSize = 32 * ByteSize.MB; // avoid to download via blob-trick >32MB to avoid memory pressure - const preferFileSystemAccessWebApis = stat.isDirectory || stat.size > maxBlobDownloadSize; - - // Folder: use FS APIs to download files and folders if available and preferred - if (preferFileSystemAccessWebApis && WebFileSystemAccess.supported(window)) { - - interface IDownloadOperation { - startTime: number; - progressScheduler: RunOnceWorker; - - filesTotal: number; - filesDownloaded: number; - - totalBytesDownloaded: number; - fileBytesDownloaded: number; - } - - async function downloadFileBuffered(resource: URI, target: FileSystemWritableFileStream, operation: IDownloadOperation): Promise { - const contents = await fileService.readFileStream(resource); - if (cts.token.isCancellationRequested) { - target.close(); - return; - } - - return new Promise((resolve, reject) => { - const sourceStream = contents.value; - - const disposables = new DisposableStore(); - disposables.add(toDisposable(() => target.close())); - - let disposed = false; - disposables.add(toDisposable(() => disposed = true)); - - disposables.add(once(cts.token.onCancellationRequested)(() => { - disposables.dispose(); - reject(); - })); - - listenStream(sourceStream, { - onData: data => { - if (!disposed) { - target.write(data.buffer); - reportProgress(contents.name, contents.size, data.byteLength, operation); - } - }, - onError: error => { - disposables.dispose(); - reject(error); - }, - onEnd: () => { - disposables.dispose(); - resolve(); - } - }); - }); - } - - async function downloadFileUnbuffered(resource: URI, target: FileSystemWritableFileStream, operation: IDownloadOperation): Promise { - const contents = await fileService.readFile(resource); - if (!cts.token.isCancellationRequested) { - target.write(contents.value.buffer); - reportProgress(contents.name, contents.size, contents.value.byteLength, operation); - } - - target.close(); - } - - async function downloadFile(targetFolder: FileSystemDirectoryHandle, file: IFileStatWithMetadata, operation: IDownloadOperation): Promise { - - // Report progress - operation.filesDownloaded++; - operation.fileBytesDownloaded = 0; // reset for this file - reportProgress(file.name, 0, 0, operation); - - // Start to download - const targetFile = await targetFolder.getFileHandle(file.name, { create: true }); - const targetFileWriter = await targetFile.createWritable(); - - // For large files, write buffered using streams - if (file.size > ByteSize.MB) { - return downloadFileBuffered(file.resource, targetFileWriter, operation); - } - - // For small files prefer to write unbuffered to reduce overhead - return downloadFileUnbuffered(file.resource, targetFileWriter, operation); - } - - async function downloadFolder(folder: IFileStatWithMetadata, targetFolder: FileSystemDirectoryHandle, operation: IDownloadOperation): Promise { - if (folder.children) { - operation.filesTotal += (folder.children.map(child => child.isFile)).length; - - for (const child of folder.children) { - if (cts.token.isCancellationRequested) { - return; - } - - if (child.isFile) { - await downloadFile(targetFolder, child, operation); - } else { - const childFolder = await targetFolder.getDirectoryHandle(child.name, { create: true }); - const resolvedChildFolder = await fileService.resolve(child.resource, { resolveMetadata: true }); - - await downloadFolder(resolvedChildFolder, childFolder, operation); - } - } - } - } - - function reportProgress(name: string, fileSize: number, bytesDownloaded: number, operation: IDownloadOperation): void { - operation.fileBytesDownloaded += bytesDownloaded; - operation.totalBytesDownloaded += bytesDownloaded; - - const bytesDownloadedPerSecond = operation.totalBytesDownloaded / ((Date.now() - operation.startTime) / 1000); - - // Small file - let message: string; - if (fileSize < ByteSize.MB) { - if (operation.filesTotal === 1) { - message = name; - } else { - message = nls.localize('downloadProgressSmallMany', "{0} of {1} files ({2}/s)", operation.filesDownloaded, operation.filesTotal, ByteSize.formatSize(bytesDownloadedPerSecond)); - } - } - - // Large file - else { - message = nls.localize('downloadProgressLarge', "{0} ({1} of {2}, {3}/s)", name, ByteSize.formatSize(operation.fileBytesDownloaded), ByteSize.formatSize(fileSize), ByteSize.formatSize(bytesDownloadedPerSecond)); - } - - // Report progress but limit to update only once per second - operation.progressScheduler.work({ message }); - } - - try { - const parentFolder: FileSystemDirectoryHandle = await window.showDirectoryPicker(); - const operation: IDownloadOperation = { - startTime: Date.now(), - progressScheduler: new RunOnceWorker(steps => { progress.report(steps[steps.length - 1]); }, 1000), - - filesTotal: stat.isDirectory ? 0 : 1, // folders increment filesTotal within downloadFolder method - filesDownloaded: 0, - - totalBytesDownloaded: 0, - fileBytesDownloaded: 0 - }; - - if (stat.isDirectory) { - const targetFolder = await parentFolder.getDirectoryHandle(stat.name, { create: true }); - await downloadFolder(stat, targetFolder, operation); - } else { - await downloadFile(parentFolder, stat, operation); - } - - operation.progressScheduler.dispose(); - } catch (error) { - logService.warn(error); - cts.cancel(); // `showDirectoryPicker` will throw an error when the user cancels - } - } - - // File: use traditional download to circumvent browser limitations - else if (stat.isFile) { - let bufferOrUri: Uint8Array | URI; - try { - bufferOrUri = (await fileService.readFile(stat.resource, { limits: { size: maxBlobDownloadSize } })).value.buffer; - } catch (error) { - bufferOrUri = FileAccess.asBrowserUri(stat.resource); - } - - if (!cts.token.isCancellationRequested) { - triggerDownload(bufferOrUri, stat.name); - } - } - } - - // Native: use working copy file service to get at the contents - else { - progress.report({ message: explorerItem.name }); - - let defaultUri = explorerItem.isDirectory ? await fileDialogService.defaultFolderPath(Schemas.file) : await fileDialogService.defaultFilePath(Schemas.file); - defaultUri = resources.joinPath(defaultUri, explorerItem.name); - - const destination = await fileDialogService.showSaveDialog({ - availableFileSystems: [Schemas.file], - saveLabel: mnemonicButtonLabel(nls.localize('downloadButton', "Download")), - title: nls.localize('chooseWhereToDownload', "Choose Where to Download"), - defaultUri - }); - - if (destination) { - 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 - } - } - })); - }, () => cts.dispose(true)); + const downloadHandler = instantiationService.createInstance(FileDownload); + return downloadHandler.download(explorerItems); }; CommandsRegistry.registerCommand({ @@ -1206,6 +944,25 @@ CommandsRegistry.registerCommand({ handler: downloadFileHandler }); +const uploadFileHandler = async (accessor: ServicesAccessor) => { + const explorerService = accessor.get(IExplorerService); + const instantiationService = accessor.get(IInstantiationService); + + const context = explorerService.getContext(true); + const element = context.length ? context[0] : explorerService.roots[0]; + + const files = await triggerUpload(); + if (files) { + const browserUpload = instantiationService.createInstance(BrowserFileUpload); + return browserUpload.upload(element, files); + } +}; + +CommandsRegistry.registerCommand({ + id: UPLOAD_COMMAND_ID, + handler: uploadFileHandler +}); + export const pasteFileHandler = async (accessor: ServicesAccessor) => { const clipboardService = accessor.get(IClipboardService); const explorerService = accessor.get(IExplorerService); diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index 086d81df5e..82c5e933a0 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -5,7 +5,8 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; -import { EditorResourceAccessor, IEditorCommandsContext, SideBySideEditor, IEditorIdentifier, SaveReason, SideBySideEditorInput, EditorsOrder } from 'vs/workbench/common/editor'; +import { EditorResourceAccessor, IEditorCommandsContext, SideBySideEditor, IEditorIdentifier, SaveReason, EditorsOrder, EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { IWindowOpenable, IOpenWindowOptions, isWorkspaceToOpen, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -88,7 +89,7 @@ export const NEXT_COMPRESSED_FOLDER = 'nextCompressedFolder'; export const FIRST_COMPRESSED_FOLDER = 'firstCompressedFolder'; export const LAST_COMPRESSED_FOLDER = 'lastCompressedFolder'; export const NEW_UNTITLED_FILE_COMMAND_ID = 'workbench.action.files.newUntitledFile'; -export const NEW_UNTITLED_PLAIN_FILE_COMMAND_ID = 'workbench.action.files.newUntitledPlainFile'; +export const NEW_UNTITLED_PLAIN_FILE_COMMAND_ID = 'workbench.action.files.newUntitledPlainFile'; // {{SQL CARBON EDIT}} export const NEW_UNTITLED_FILE_LABEL = nls.localize('newUntitledFile', "New Untitled File"); export const openWindowCommand = (accessor: ServicesAccessor, toOpen: IWindowOpenable[], options?: IOpenWindowOptions) => { @@ -245,8 +246,8 @@ CommandsRegistry.registerCommand({ if (resources.length === 2) { return editorService.openEditor({ - leftResource: resources[0], - rightResource: resources[1], + originalInput: { resource: resources[0] }, + modifiedInput: { resource: resources[1] }, options: { pinned: true } }); } @@ -264,8 +265,8 @@ CommandsRegistry.registerCommand({ const rightResource = getResourceForCommand(resource, listService, editorService); if (globalResourceToCompare && rightResource) { editorService.openEditor({ - leftResource: globalResourceToCompare, - rightResource, + originalInput: { resource: globalResourceToCompare }, + modifiedInput: { resource: rightResource }, options: { pinned: true } }); } @@ -388,7 +389,7 @@ async function saveSelectedEditors(accessor: ServicesAccessor, options?: ISaveEd // See also https://github.com/microsoft/vscode/issues/106330 if ( activeGroup.activeEditor instanceof SideBySideEditorInput && - !options?.saveAs && !(activeGroup.activeEditor.primary.isUntitled() || activeGroup.activeEditor.secondary.isUntitled()) + !options?.saveAs && !(activeGroup.activeEditor.primary.hasCapability(EditorInputCapabilities.Untitled) || activeGroup.activeEditor.secondary.hasCapability(EditorInputCapabilities.Untitled)) ) { editors.push({ groupId: activeGroup.id, editor: activeGroup.activeEditor.primary }); editors.push({ groupId: activeGroup.id, editor: activeGroup.activeEditor.secondary }); @@ -552,7 +553,7 @@ CommandsRegistry.registerCommand({ } try { - await editorService.revert(editors.filter(({ editor }) => !editor.isUntitled() /* all except untitled */), { force: true }); + await editorService.revert(editors.filter(({ editor }) => !editor.hasCapability(EditorInputCapabilities.Untitled) /* all except untitled */), { force: true }); } catch (error) { notificationService.error(nls.localize('genericRevertError', "Failed to revert '{0}': {1}", editors.map(({ editor }) => editor.getName()).join(', '), toErrorMessage(error, false))); } diff --git a/src/vs/workbench/contrib/files/browser/fileImportExport.ts b/src/vs/workbench/contrib/files/browser/fileImportExport.ts new file mode 100644 index 0000000000..c4270505db --- /dev/null +++ b/src/vs/workbench/contrib/files/browser/fileImportExport.ts @@ -0,0 +1,825 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { getFileNamesMessage, IConfirmation, IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ByteSize, FileSystemProviderCapabilities, IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; +import { Severity } from 'vs/platform/notification/common/notification'; +import { IProgress, IProgressService, IProgressStep, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; +import { VIEW_ID } from 'vs/workbench/contrib/files/common/files'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { Limiter, Promises, RunOnceWorker } from 'vs/base/common/async'; +import { newWriteableBufferStream, VSBuffer } from 'vs/base/common/buffer'; +import { basename, joinPath } from 'vs/base/common/resources'; +import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; +import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; +import { URI } from 'vs/base/common/uri'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { extractEditorsDropData } from 'vs/workbench/browser/dnd'; +import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; +import { isWeb } from 'vs/base/common/platform'; +import { triggerDownload, WebFileSystemAccess } from 'vs/base/browser/dom'; +import { ILogService } from 'vs/platform/log/common/log'; +import { FileAccess, Schemas } from 'vs/base/common/network'; +import { mnemonicButtonLabel } from 'vs/base/common/labels'; +import { listenStream } from 'vs/base/common/stream'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { once } from 'vs/base/common/functional'; +import { coalesce } from 'vs/base/common/arrays'; + +//#region Browser File Upload (drag and drop, input element) + +interface IBrowserUploadOperation { + startTime: number; + progressScheduler: RunOnceWorker; + + filesTotal: number; + filesUploaded: number; + + totalBytesUploaded: number; +} + +interface IWebkitDataTransfer { + items: IWebkitDataTransferItem[]; +} + +interface IWebkitDataTransferItem { + webkitGetAsEntry(): IWebkitDataTransferItemEntry; +} + +interface IWebkitDataTransferItemEntry { + name: string | undefined; + isFile: boolean; + isDirectory: boolean; + + file(resolve: (file: File) => void, reject: () => void): void; + createReader(): IWebkitDataTransferItemEntryReader; +} + +interface IWebkitDataTransferItemEntryReader { + readEntries(resolve: (file: IWebkitDataTransferItemEntry[]) => void, reject: () => void): void +} + +export class BrowserFileUpload { + + private static readonly MAX_PARALLEL_UPLOADS = 20; + + constructor( + @IProgressService private readonly progressService: IProgressService, + @IDialogService private readonly dialogService: IDialogService, + @IExplorerService private readonly explorerService: IExplorerService, + @IEditorService private readonly editorService: IEditorService, + @IFileService private readonly fileService: IFileService + ) { + } + + upload(target: ExplorerItem, source: DragEvent | FileList): Promise { + const cts = new CancellationTokenSource(); + + // Indicate progress globally + const uploadPromise = this.progressService.withProgress( + { + location: ProgressLocation.Window, + delay: 800, + cancellable: true, + title: localize('uploadingFiles', "Uploading") + }, + async progress => this.doUpload(target, this.toTransfer(source), progress, cts.token), + () => cts.dispose(true) + ); + + // Also indicate progress in the files view + this.progressService.withProgress({ location: VIEW_ID, delay: 500 }, () => uploadPromise); + + return uploadPromise; + } + + private toTransfer(source: DragEvent | FileList): IWebkitDataTransfer { + if (source instanceof DragEvent) { + return source.dataTransfer as unknown as IWebkitDataTransfer; + } + + const transfer: IWebkitDataTransfer = { items: [] }; + + // We want to reuse the same code for uploading from + // Drag & Drop as well as input element based upload + // so we convert into webkit data transfer when the + // input element approach is used (simplified). + for (const file of source) { + transfer.items.push({ + webkitGetAsEntry: () => { + return { + name: file.name, + isDirectory: false, + isFile: true, + createReader: () => { throw new Error('Unsupported for files'); }, + file: resolve => resolve(file) + }; + } + }); + } + + return transfer; + } + + private async doUpload(target: ExplorerItem, source: IWebkitDataTransfer, progress: IProgress, token: CancellationToken): Promise { + const items = source.items; + + // Somehow the items thing is being modified at random, maybe as a security + // measure since this is a DND operation. As such, we copy the items into + // an array we own as early as possible before using it. + const entries: IWebkitDataTransferItemEntry[] = []; + for (const item of items) { + entries.push(item.webkitGetAsEntry()); + } + + const results: { isFile: boolean, resource: URI }[] = []; + const operation: IBrowserUploadOperation = { + startTime: Date.now(), + progressScheduler: new RunOnceWorker(steps => { progress.report(steps[steps.length - 1]); }, 1000), + + filesTotal: entries.length, + filesUploaded: 0, + + totalBytesUploaded: 0 + }; + + // Upload all entries in parallel up to a + // certain maximum leveraging the `Limiter` + const uploadLimiter = new Limiter(BrowserFileUpload.MAX_PARALLEL_UPLOADS); + await Promises.settled(entries.map(entry => { + return uploadLimiter.queue(async () => { + if (token.isCancellationRequested) { + return; + } + + // Confirm overwrite as needed + if (target && entry.name && target.getChild(entry.name)) { + const { confirmed } = await this.dialogService.confirm(getFileOverwriteConfirm(entry.name)); + if (!confirmed) { + return; + } + + await this.explorerService.applyBulkEdit([new ResourceFileEdit(joinPath(target.resource, entry.name), undefined, { recursive: true, folder: target.getChild(entry.name)?.isDirectory })], { + undoLabel: localize('overwrite', "Overwrite {0}", entry.name), + progressLabel: localize('overwriting', "Overwriting {0}", entry.name), + }); + + if (token.isCancellationRequested) { + return; + } + } + + // Upload entry + const result = await this.doUploadEntry(entry, target.resource, target, progress, operation, token); + if (result) { + results.push(result); + } + }); + })); + + operation.progressScheduler.dispose(); + + // Open uploaded file in editor only if we upload just one + const firstUploadedFile = results[0]; + if (!token.isCancellationRequested && firstUploadedFile?.isFile) { + await this.editorService.openEditor({ resource: firstUploadedFile.resource, options: { pinned: true } }); + } + } + + private async doUploadEntry(entry: IWebkitDataTransferItemEntry, parentResource: URI, target: ExplorerItem | undefined, progress: IProgress, operation: IBrowserUploadOperation, token: CancellationToken): Promise<{ isFile: boolean, resource: URI } | undefined> { + if (token.isCancellationRequested || !entry.name || (!entry.isFile && !entry.isDirectory)) { + return undefined; + } + + // Report progress + let fileBytesUploaded = 0; + const reportProgress = (fileSize: number, bytesUploaded: number): void => { + fileBytesUploaded += bytesUploaded; + operation.totalBytesUploaded += bytesUploaded; + + const bytesUploadedPerSecond = operation.totalBytesUploaded / ((Date.now() - operation.startTime) / 1000); + + // Small file + let message: string; + if (fileSize < ByteSize.MB) { + if (operation.filesTotal === 1) { + message = `${entry.name}`; + } else { + message = localize('uploadProgressSmallMany', "{0} of {1} files ({2}/s)", operation.filesUploaded, operation.filesTotal, ByteSize.formatSize(bytesUploadedPerSecond)); + } + } + + // Large file + else { + message = localize('uploadProgressLarge', "{0} ({1} of {2}, {3}/s)", entry.name, ByteSize.formatSize(fileBytesUploaded), ByteSize.formatSize(fileSize), ByteSize.formatSize(bytesUploadedPerSecond)); + } + + // Report progress but limit to update only once per second + operation.progressScheduler.work({ message }); + }; + operation.filesUploaded++; + reportProgress(0, 0); + + // Handle file upload + const resource = joinPath(parentResource, entry.name); + if (entry.isFile) { + const file = await new Promise((resolve, reject) => entry.file(resolve, reject)); + + if (token.isCancellationRequested) { + return undefined; + } + + // Chrome/Edge/Firefox support stream method, but only use it for + // larger files to reduce the overhead of the streaming approach + if (typeof file.stream === 'function' && file.size > ByteSize.MB) { + await this.doUploadFileBuffered(resource, file, reportProgress, token); + } + + // Fallback to unbuffered upload for other browsers or small files + else { + await this.doUploadFileUnbuffered(resource, file, reportProgress); + } + + return { isFile: true, resource }; + } + + // Handle folder upload + else { + + // Create target folder + await this.fileService.createFolder(resource); + + if (token.isCancellationRequested) { + return undefined; + } + + // Recursive upload files in this directory + const dirReader = entry.createReader(); + const childEntries: IWebkitDataTransferItemEntry[] = []; + let done = false; + do { + const childEntriesChunk = await new Promise((resolve, reject) => dirReader.readEntries(resolve, reject)); + if (childEntriesChunk.length > 0) { + childEntries.push(...childEntriesChunk); + } else { + done = true; // an empty array is a signal that all entries have been read + } + } while (!done && !token.isCancellationRequested); + + // Update operation total based on new counts + operation.filesTotal += childEntries.length; + + // Split up files from folders to upload + const folderTarget = target && target.getChild(entry.name) || undefined; + const fileChildEntries: IWebkitDataTransferItemEntry[] = []; + const folderChildEntries: IWebkitDataTransferItemEntry[] = []; + for (const childEntry of childEntries) { + if (childEntry.isFile) { + fileChildEntries.push(childEntry); + } else if (childEntry.isDirectory) { + folderChildEntries.push(childEntry); + } + } + + // Upload files (up to `MAX_PARALLEL_UPLOADS` in parallel) + const fileUploadQueue = new Limiter(BrowserFileUpload.MAX_PARALLEL_UPLOADS); + await Promises.settled(fileChildEntries.map(fileChildEntry => { + return fileUploadQueue.queue(() => this.doUploadEntry(fileChildEntry, resource, folderTarget, progress, operation, token)); + })); + + // Upload folders (sequentially give we don't know their sizes) + for (const folderChildEntry of folderChildEntries) { + await this.doUploadEntry(folderChildEntry, resource, folderTarget, progress, operation, token); + } + + return { isFile: false, resource }; + } + } + + private async doUploadFileBuffered(resource: URI, file: File, progressReporter: (fileSize: number, bytesUploaded: number) => void, token: CancellationToken): Promise { + const writeableStream = newWriteableBufferStream({ + // Set a highWaterMark to prevent the stream + // for file upload to produce large buffers + // in-memory + highWaterMark: 10 + }); + const writeFilePromise = this.fileService.writeFile(resource, writeableStream); + + // Read the file in chunks using File.stream() web APIs + try { + const reader: ReadableStreamDefaultReader = file.stream().getReader(); + + let res = await reader.read(); + while (!res.done) { + if (token.isCancellationRequested) { + return undefined; + } + + // Write buffer into stream but make sure to wait + // in case the highWaterMark is reached + const buffer = VSBuffer.wrap(res.value); + await writeableStream.write(buffer); + + if (token.isCancellationRequested) { + return undefined; + } + + // Report progress + progressReporter(file.size, buffer.byteLength); + + res = await reader.read(); + } + writeableStream.end(undefined); + } catch (error) { + writeableStream.error(error); + writeableStream.end(); + } + + if (token.isCancellationRequested) { + return undefined; + } + + // Wait for file being written to target + await writeFilePromise; + } + + private doUploadFileUnbuffered(resource: URI, file: File, progressReporter: (fileSize: number, bytesUploaded: number) => void): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = async event => { + try { + if (event.target?.result instanceof ArrayBuffer) { + const buffer = VSBuffer.wrap(new Uint8Array(event.target.result)); + await this.fileService.writeFile(resource, buffer); + + // Report progress + progressReporter(file.size, buffer.byteLength); + } else { + throw new Error('Could not read from dropped file.'); + } + + resolve(); + } catch (error) { + reject(error); + } + }; + + // Start reading the file to trigger `onload` + reader.readAsArrayBuffer(file); + }); + } +} + +//#endregion + +//#region Native File Import (drag and drop) + +export class NativeFileImport { + + constructor( + @IFileService private readonly fileService: IFileService, + @IHostService private readonly hostService: IHostService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @IDialogService private readonly dialogService: IDialogService, + @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService, + @IExplorerService private readonly explorerService: IExplorerService, + @IEditorService private readonly editorService: IEditorService, + @IProgressService private readonly progressService: IProgressService + ) { + } + + async import(target: ExplorerItem, source: DragEvent): Promise { + const cts = new CancellationTokenSource(); + + // Indicate progress globally + const importPromise = this.progressService.withProgress( + { + location: ProgressLocation.Window, + delay: 800, + cancellable: true, + title: localize('copyingFiles', "Copying...") + }, + async () => await this.doImport(target, source, cts.token), + () => cts.dispose(true) + ); + + // Also indicate progress in the files view + this.progressService.withProgress({ location: VIEW_ID, delay: 500 }, () => importPromise); + + return importPromise; + } + + private async doImport(target: ExplorerItem, source: DragEvent, token: CancellationToken): Promise { + + // Check for dropped external files to be folders + const files = coalesce(extractEditorsDropData(source, true).filter(editor => URI.isUri(editor.resource) && this.fileService.canHandleResource(editor.resource)).map(editor => editor.resource)); + const resolvedFiles = await this.fileService.resolveAll(files.map(file => ({ resource: file }))); + + if (token.isCancellationRequested) { + return; + } + + // Pass focus to window + this.hostService.focus(); + + // Handle folders by adding to workspace if we are in workspace context and if dropped on top + const folders = resolvedFiles.filter(resolvedFile => resolvedFile.success && resolvedFile.stat?.isDirectory).map(resolvedFile => ({ uri: resolvedFile.stat!.resource })); + if (folders.length > 0 && target.isRoot) { + const buttons = [ + folders.length > 1 ? + localize('copyFolders', "&&Copy Folders") : + localize('copyFolder', "&&Copy Folder"), + localize('cancel', "Cancel") + ]; + + let message: string; + + // We only allow to add a folder to the workspace if there is already a workspace folder with that scheme + const workspaceFolderSchemas = this.contextService.getWorkspace().folders.map(folder => folder.uri.scheme); + if (folders.some(folder => workspaceFolderSchemas.indexOf(folder.uri.scheme) >= 0)) { + buttons.unshift(folders.length > 1 ? localize('addFolders', "&&Add Folders to Workspace") : localize('addFolder', "&&Add Folder to Workspace")); + message = folders.length > 1 ? + localize('dropFolders', "Do you want to copy the folders or add the folders to the workspace?") : + localize('dropFolder', "Do you want to copy '{0}' or add '{0}' as a folder to the workspace?", basename(folders[0].uri)); + } else { + message = folders.length > 1 ? + localize('copyfolders', "Are you sure to want to copy folders?") : + localize('copyfolder', "Are you sure to want to copy '{0}'?", basename(folders[0].uri)); + } + + const { choice } = await this.dialogService.show(Severity.Info, message, buttons); + + // Add folders + if (choice === buttons.length - 3) { + return this.workspaceEditingService.addFolders(folders); + } + + // Copy resources + if (choice === buttons.length - 2) { + return this.importResources(target, files, token); + } + } + + // Handle dropped files (only support FileStat as target) + else if (target instanceof ExplorerItem) { + return this.importResources(target, files, token); + } + } + + private async importResources(target: ExplorerItem, resources: URI[], token: CancellationToken): Promise { + if (resources && resources.length > 0) { + + // Resolve target to check for name collisions and ask user + const targetStat = await this.fileService.resolve(target.resource); + + if (token.isCancellationRequested) { + return; + } + + // Check for name collisions + const targetNames = new Set(); + const caseSensitive = this.fileService.hasCapability(target.resource, FileSystemProviderCapabilities.PathCaseSensitive); + if (targetStat.children) { + targetStat.children.forEach(child => { + targetNames.add(caseSensitive ? child.name : child.name.toLowerCase()); + }); + } + + const resourcesFiltered = coalesce((await Promises.settled(resources.map(async resource => { + if (targetNames.has(caseSensitive ? basename(resource) : basename(resource).toLowerCase())) { + const confirmationResult = await this.dialogService.confirm(getFileOverwriteConfirm(basename(resource))); + if (!confirmationResult.confirmed) { + return undefined; + } + } + + return resource; + })))); + + // Copy resources through bulk edit API + const resourceFileEdits = resourcesFiltered.map(resource => { + const sourceFileName = basename(resource); + const targetFile = joinPath(target.resource, sourceFileName); + + return new ResourceFileEdit(resource, targetFile, { overwrite: true, copy: true }); + }); + + await this.explorerService.applyBulkEdit(resourceFileEdits, { + 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), + progressLocation: ProgressLocation.Window + }); + + // if we only add one file, just open it directly + if (resourceFileEdits.length === 1) { + const item = this.explorerService.findClosest(resourceFileEdits[0].newResource!); + if (item && !item.isDirectory) { + this.editorService.openEditor({ resource: item.resource, options: { pinned: true } }); + } + } + } + } +} + +//#endregion + +//#region Download (web, native) + +interface IDownloadOperation { + startTime: number; + progressScheduler: RunOnceWorker; + + filesTotal: number; + filesDownloaded: number; + + totalBytesDownloaded: number; + fileBytesDownloaded: number; +} + +export class FileDownload { + + constructor( + @IFileService private readonly fileService: IFileService, + @IExplorerService private readonly explorerService: IExplorerService, + @IProgressService private readonly progressService: IProgressService, + @ILogService private readonly logService: ILogService, + @IFileDialogService private readonly fileDialogService: IFileDialogService + ) { + } + + download(source: ExplorerItem[]): Promise { + const cts = new CancellationTokenSource(); + + // Indicate progress globally + const downloadPromise = this.progressService.withProgress( + { + location: ProgressLocation.Window, + delay: 800, + cancellable: isWeb, + title: localize('downloadingFiles', "Downloading") + }, + async progress => this.doDownload(source, progress, cts), + () => cts.dispose(true) + ); + + // Also indicate progress in the files view + this.progressService.withProgress({ location: VIEW_ID, delay: 500 }, () => downloadPromise); + + return downloadPromise; + } + + private async doDownload(sources: ExplorerItem[], progress: IProgress, cts: CancellationTokenSource): Promise { + for (const source of sources) { + if (cts.token.isCancellationRequested) { + return; + } + + // Web: use DOM APIs to download files with optional support + // for folders and large files + if (isWeb) { + await this.doDownloadBrowser(source.resource, progress, cts); + } + + // Native: use working copy file service to get at the contents + else { + await this.doDownloadNative(source, progress, cts); + } + } + } + + private async doDownloadBrowser(resource: URI, progress: IProgress, cts: CancellationTokenSource): Promise { + const stat = await this.fileService.resolve(resource, { resolveMetadata: true }); + + if (cts.token.isCancellationRequested) { + return; + } + + const maxBlobDownloadSize = 32 * ByteSize.MB; // avoid to download via blob-trick >32MB to avoid memory pressure + const preferFileSystemAccessWebApis = stat.isDirectory || stat.size > maxBlobDownloadSize; + + // Folder: use FS APIs to download files and folders if available and preferred + if (preferFileSystemAccessWebApis && WebFileSystemAccess.supported(window)) { + try { + const parentFolder: FileSystemDirectoryHandle = await window.showDirectoryPicker(); + const operation: IDownloadOperation = { + startTime: Date.now(), + progressScheduler: new RunOnceWorker(steps => { progress.report(steps[steps.length - 1]); }, 1000), + + filesTotal: stat.isDirectory ? 0 : 1, // folders increment filesTotal within downloadFolder method + filesDownloaded: 0, + + totalBytesDownloaded: 0, + fileBytesDownloaded: 0 + }; + + if (stat.isDirectory) { + const targetFolder = await parentFolder.getDirectoryHandle(stat.name, { create: true }); + await this.downloadFolderBrowser(stat, targetFolder, operation, cts.token); + } else { + await this.downloadFileBrowser(parentFolder, stat, operation, cts.token); + } + + operation.progressScheduler.dispose(); + } catch (error) { + this.logService.warn(error); + cts.cancel(); // `showDirectoryPicker` will throw an error when the user cancels + } + } + + // File: use traditional download to circumvent browser limitations + else if (stat.isFile) { + let bufferOrUri: Uint8Array | URI; + try { + bufferOrUri = (await this.fileService.readFile(stat.resource, { limits: { size: maxBlobDownloadSize } })).value.buffer; + } catch (error) { + bufferOrUri = FileAccess.asBrowserUri(stat.resource); + } + + if (!cts.token.isCancellationRequested) { + triggerDownload(bufferOrUri, stat.name); + } + } + } + + private async downloadFileBufferedBrowser(resource: URI, target: FileSystemWritableFileStream, operation: IDownloadOperation, token: CancellationToken): Promise { + const contents = await this.fileService.readFileStream(resource); + if (token.isCancellationRequested) { + target.close(); + return; + } + + return new Promise((resolve, reject) => { + const sourceStream = contents.value; + + const disposables = new DisposableStore(); + disposables.add(toDisposable(() => target.close())); + + let disposed = false; + disposables.add(toDisposable(() => disposed = true)); + + disposables.add(once(token.onCancellationRequested)(() => { + disposables.dispose(); + reject(); + })); + + listenStream(sourceStream, { + onData: data => { + if (!disposed) { + target.write(data.buffer); + this.reportProgress(contents.name, contents.size, data.byteLength, operation); + } + }, + onError: error => { + disposables.dispose(); + reject(error); + }, + onEnd: () => { + disposables.dispose(); + resolve(); + } + }); + }); + } + + private async downloadFileUnbufferedBrowser(resource: URI, target: FileSystemWritableFileStream, operation: IDownloadOperation, token: CancellationToken): Promise { + const contents = await this.fileService.readFile(resource); + if (!token.isCancellationRequested) { + target.write(contents.value.buffer); + this.reportProgress(contents.name, contents.size, contents.value.byteLength, operation); + } + + target.close(); + } + + private async downloadFileBrowser(targetFolder: FileSystemDirectoryHandle, file: IFileStatWithMetadata, operation: IDownloadOperation, token: CancellationToken): Promise { + + // Report progress + operation.filesDownloaded++; + operation.fileBytesDownloaded = 0; // reset for this file + this.reportProgress(file.name, 0, 0, operation); + + // Start to download + const targetFile = await targetFolder.getFileHandle(file.name, { create: true }); + const targetFileWriter = await targetFile.createWritable(); + + // For large files, write buffered using streams + if (file.size > ByteSize.MB) { + return this.downloadFileBufferedBrowser(file.resource, targetFileWriter, operation, token); + } + + // For small files prefer to write unbuffered to reduce overhead + return this.downloadFileUnbufferedBrowser(file.resource, targetFileWriter, operation, token); + } + + private async downloadFolderBrowser(folder: IFileStatWithMetadata, targetFolder: FileSystemDirectoryHandle, operation: IDownloadOperation, token: CancellationToken): Promise { + if (folder.children) { + operation.filesTotal += (folder.children.map(child => child.isFile)).length; + + for (const child of folder.children) { + if (token.isCancellationRequested) { + return; + } + + if (child.isFile) { + await this.downloadFileBrowser(targetFolder, child, operation, token); + } else { + const childFolder = await targetFolder.getDirectoryHandle(child.name, { create: true }); + const resolvedChildFolder = await this.fileService.resolve(child.resource, { resolveMetadata: true }); + + await this.downloadFolderBrowser(resolvedChildFolder, childFolder, operation, token); + } + } + } + } + + private reportProgress(name: string, fileSize: number, bytesDownloaded: number, operation: IDownloadOperation): void { + operation.fileBytesDownloaded += bytesDownloaded; + operation.totalBytesDownloaded += bytesDownloaded; + + const bytesDownloadedPerSecond = operation.totalBytesDownloaded / ((Date.now() - operation.startTime) / 1000); + + // Small file + let message: string; + if (fileSize < ByteSize.MB) { + if (operation.filesTotal === 1) { + message = name; + } else { + message = localize('downloadProgressSmallMany', "{0} of {1} files ({2}/s)", operation.filesDownloaded, operation.filesTotal, ByteSize.formatSize(bytesDownloadedPerSecond)); + } + } + + // Large file + else { + message = localize('downloadProgressLarge', "{0} ({1} of {2}, {3}/s)", name, ByteSize.formatSize(operation.fileBytesDownloaded), ByteSize.formatSize(fileSize), ByteSize.formatSize(bytesDownloadedPerSecond)); + } + + // Report progress but limit to update only once per second + operation.progressScheduler.work({ message }); + } + + private async doDownloadNative(explorerItem: ExplorerItem, progress: IProgress, cts: CancellationTokenSource): Promise { + progress.report({ message: explorerItem.name }); + + const defaultUri = joinPath( + explorerItem.isDirectory ? + await this.fileDialogService.defaultFolderPath(Schemas.file) : + await this.fileDialogService.defaultFilePath(Schemas.file), + explorerItem.name + ); + + const destination = await this.fileDialogService.showSaveDialog({ + availableFileSystems: [Schemas.file], + saveLabel: mnemonicButtonLabel(localize('downloadButton', "Download")), + title: localize('chooseWhereToDownload', "Choose Where to Download"), + defaultUri + }); + + if (destination) { + await this.explorerService.applyBulkEdit([new ResourceFileEdit(explorerItem.resource, destination, { overwrite: true, copy: true })], { + undoLabel: localize('downloadBulkEdit', "Download {0}", explorerItem.name), + progressLabel: localize('downloadingBulkEdit', "Downloading {0}", explorerItem.name), + progressLocation: ProgressLocation.Window + }); + } else { + cts.cancel(); // User canceled a download. In case there were multiple files selected we should cancel the remainder of the prompts #86100 + } + } +} + +//#endregion + +//#region Helpers + +export function getFileOverwriteConfirm(name: string): IConfirmation { + return { + message: localize('confirmOverwrite', "A file or folder with the name '{0}' already exists in the destination folder. Do you want to replace it?", name), + detail: localize('irreversible', "This action is irreversible!"), + primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), + type: 'warning' + }; +} + +export function getMultipleFilesOverwriteConfirm(files: URI[]): IConfirmation { + if (files.length > 1) { + return { + message: localize('confirmManyOverwrites', "The following {0} files and/or folders already exist in the destination folder. Do you want to replace them?", files.length), + detail: getFileNamesMessage(files) + '\n' + localize('irreversible', "This action is irreversible!"), + primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), + type: 'warning' + }; + } + + return getFileOverwriteConfirm(basename(files[0])); +} + +//#endregion diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 3cc0bd1062..1d9ccc648d 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -8,16 +8,16 @@ import { sep } from 'vs/base/common/path'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { EditorInput, IFileEditorInput, IEditorInputFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor'; +import { IFileEditorInput, IEditorInputFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor'; import { AutoSaveConfiguration, HotExitConfiguration, FILES_EXCLUDE_CONFIG, FILES_ASSOCIATIONS_CONFIG } from 'vs/platform/files/common/files'; -import { SortOrder, FILE_EDITOR_INPUT_ID } from 'vs/workbench/contrib/files/common/files'; +import { SortOrder, LexicographicOptions, 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 { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { BinaryFileEditor } from 'vs/workbench/contrib/files/browser/editors/binaryFileEditor'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import * as platform from 'vs/base/common/platform'; +import { isNative, isWeb, isWindows } from 'vs/base/common/platform'; import { ExplorerViewletViewsContribution } from 'vs/workbench/contrib/files/browser/explorerViewlet'; import { IEditorRegistry, EditorDescriptor } from 'vs/workbench/browser/editor'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -31,8 +31,9 @@ import { editorConfigurationBaseNode } from 'vs/editor/common/config/commonEdito import { DirtyFilesIndicator } from 'vs/workbench/contrib/files/common/dirtyFilesIndicator'; import { UndoCommand, RedoCommand } from 'vs/editor/browser/editorExtensions'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { FileEditorInputSerializer, IExplorerService } from 'vs/workbench/contrib/files/browser/files'; +import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; import * as locConstants from 'sql/base/common/locConstants'; // {{SQL CARBON EDIT}} +import { FileEditorInputSerializer, FileEditorWorkingCopyEditorHandler } from 'vs/workbench/contrib/files/browser/editors/fileEditorHandler'; class FileUriLabelContribution implements IWorkbenchContribution { @@ -42,8 +43,8 @@ class FileUriLabelContribution implements IWorkbenchContribution { formatting: { label: '${authority}${path}', separator: sep, - tildify: !platform.isWindows, - normalizeDriveLetter: platform.isWindows, + tildify: !isWindows, + normalizeDriveLetter: isWindows, authorityPrefix: sep + sep, workspaceSuffix: '' } @@ -61,7 +62,7 @@ Registry.as(EditorExtensions.Editors).registerEditor( nls.localize('binaryFileEditor', "Binary File Editor") ), [ - new SyncDescriptor(FileEditorInput) + new SyncDescriptor(FileEditorInput) ] ); @@ -70,8 +71,8 @@ Registry.as(EditorExtensions.EditorInputFactories). typeId: FILE_EDITOR_INPUT_ID, - createFileEditorInput: (resource, preferredResource, preferredName, preferredDescription, preferredEncoding, preferredMode, instantiationService): IFileEditorInput => { - return instantiationService.createInstance(FileEditorInput, resource, preferredResource, preferredName, preferredDescription, preferredEncoding, preferredMode); + createFileEditorInput: (resource, preferredResource, preferredName, preferredDescription, preferredEncoding, preferredMode, preferredContents, instantiationService): IFileEditorInput => { + return instantiationService.createInstance(FileEditorInput, resource, preferredResource, preferredName, preferredDescription, preferredEncoding, preferredMode, preferredContents); }, isFileEditorInput: (obj): obj is IFileEditorInput => { @@ -79,8 +80,9 @@ Registry.as(EditorExtensions.EditorInputFactories). } }); -// Register Editor Input Serializer +// Register Editor Input Serializer & Handler Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputSerializer(FILE_EDITOR_INPUT_ID, FileEditorInputSerializer); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(FileEditorWorkingCopyEditorHandler, LifecyclePhase.Ready); // Register Explorer views Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ExplorerViewletViewsContribution, LifecyclePhase.Starting); @@ -103,7 +105,7 @@ Registry.as(WorkbenchExtensions.Workbench).regi // Configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); -const hotExitConfiguration: IConfigurationPropertySchema = platform.isNative ? +const hotExitConfiguration: IConfigurationPropertySchema = isNative ? { 'type': 'string', 'scope': ConfigurationScope.APPLICATION, @@ -228,7 +230,7 @@ configurationRegistry.registerConfiguration({ nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.onFocusChange' }, "A dirty editor is automatically saved when the editor loses focus."), nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.onWindowChange' }, "A dirty editor is automatically saved when the window loses focus.") ], - 'default': platform.isWeb ? AutoSaveConfiguration.AFTER_DELAY : AutoSaveConfiguration.OFF, + 'default': isWeb ? AutoSaveConfiguration.AFTER_DELAY : AutoSaveConfiguration.OFF, 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSave' }, "Controls auto save of dirty editors. Read more about autosave [here](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save).", AutoSaveConfiguration.OFF, AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE, AutoSaveConfiguration.AFTER_DELAY) }, 'files.autoSaveDelay': { @@ -238,7 +240,7 @@ configurationRegistry.registerConfiguration({ }, 'files.watcherExclude': { 'type': 'object', - 'default': platform.isWindows /* https://github.com/microsoft/vscode/issues/23954 */ ? { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/*/**': true, '**/.hg/store/**': true } : { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/**': true, '**/.hg/store/**': true }, + 'default': isWindows /* https://github.com/microsoft/vscode/issues/23954 */ ? { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/*/**': true, '**/.hg/store/**': true } : { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/**': true, '**/.hg/store/**': true }, 'description': nls.localize('watcherExclude', "Configure glob patterns of file paths to exclude from file watching. Patterns must match on absolute paths (i.e. prefix with ** or the full path to match properly). Changing this setting requires a restart. When you experience Code consuming lots of CPU time on startup, you can exclude large folders to reduce the initial load."), 'scope': ConfigurationScope.RESOURCE }, @@ -251,7 +253,7 @@ configurationRegistry.registerConfiguration({ 'type': 'number', 'default': 4096, 'markdownDescription': locConstants.filesContributionMaxMemoryForLargeFilesMB, // {{SQL CARBON EDIT}} Change product name to ADS - included: platform.isNative + included: isNative }, 'files.restoreUndoStack': { 'type': 'boolean', @@ -357,13 +359,25 @@ configurationRegistry.registerConfiguration({ 'enum': [SortOrder.Default, SortOrder.Mixed, SortOrder.FilesFirst, SortOrder.Type, SortOrder.Modified], 'default': SortOrder.Default, 'enumDescriptions': [ - nls.localize('sortOrder.default', 'Files and folders are sorted by their names, in alphabetical order. Folders are displayed before files.'), - nls.localize('sortOrder.mixed', 'Files and folders are sorted by their names, in alphabetical order. Files are interwoven with folders.'), - nls.localize('sortOrder.filesFirst', 'Files and folders are sorted by their names, in alphabetical order. Files are displayed before folders.'), - nls.localize('sortOrder.type', 'Files and folders are sorted by their extensions, in alphabetical order. Folders are displayed before files.'), - nls.localize('sortOrder.modified', 'Files and folders are sorted by last modified date, in descending order. Folders are displayed before files.') + nls.localize('sortOrder.default', 'Files and folders are sorted by their names. Folders are displayed before files.'), + nls.localize('sortOrder.mixed', 'Files and folders are sorted by their names. Files are interwoven with folders.'), + nls.localize('sortOrder.filesFirst', 'Files and folders are sorted by their names. Files are displayed before folders.'), + nls.localize('sortOrder.type', 'Files and folders are grouped by extension type then sorted by their names. Folders are displayed before files.'), + nls.localize('sortOrder.modified', 'Files and folders are sorted by last modified date in descending order. Folders are displayed before files.') ], - 'description': nls.localize('sortOrder', "Controls sorting order of files and folders in the explorer.") + 'description': nls.localize('sortOrder', "Controls the property-based sorting of files and folders in the explorer.") + }, + 'explorer.sortOrderLexicographicOptions': { + 'type': 'string', + 'enum': [LexicographicOptions.Default, LexicographicOptions.Upper, LexicographicOptions.Lower, LexicographicOptions.Unicode], + 'default': LexicographicOptions.Default, + 'enumDescriptions': [ + nls.localize('sortOrderLexicographicOptions.default', 'Uppercase and lowercase names are mixed together.'), + nls.localize('sortOrderLexicographicOptions.upper', 'Uppercase names are grouped together before lowercase names.'), + nls.localize('sortOrderLexicographicOptions.lower', 'Lowercase names are grouped together before uppercase names.'), + nls.localize('sortOrderLexicographicOptions.unicode', 'Names are sorted in unicode order.') + ], + 'description': nls.localize('sortOrderLexicographicOptions', "Controls the lexicographic sorting of file and folder names in the explorer.") }, 'explorer.decorations.colors': { type: 'boolean', diff --git a/src/vs/workbench/contrib/files/browser/files.ts b/src/vs/workbench/contrib/files/browser/files.ts index e5d5687411..d982190c48 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, UriComponents } from 'vs/base/common/uri'; +import { URI } 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, EditorInput, IEditorInputSerializer } from 'vs/workbench/common/editor'; +import { OpenEditor, ISortOrderConfiguration } from 'vs/workbench/contrib/files/common/files'; +import { EditorResourceAccessor, SideBySideEditor, IEditorIdentifier } 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,67 +14,14 @@ 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, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { createDecorator } 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 FileEditorInputSerializer implements IEditorInputSerializer { - - canSerialize(editorInput: EditorInput): boolean { - return true; - } - - serialize(editorInput: EditorInput): string { - const fileEditorInput = editorInput as FileEditorInput; - 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; readonly roots: ExplorerItem[]; - readonly sortOrder: SortOrder; + readonly sortOrderConfiguration: ISortOrderConfiguration; getContext(respectMultiSelection: boolean): ExplorerItem[]; hasViewFocus(): boolean; diff --git a/src/vs/workbench/contrib/files/browser/files.web.contribution.ts b/src/vs/workbench/contrib/files/browser/files.web.contribution.ts index 2c03452c6b..ec2730fcf0 100644 --- a/src/vs/workbench/contrib/files/browser/files.web.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.web.contribution.ts @@ -3,10 +3,10 @@ * 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 { Registry } from 'vs/platform/registry/common/platform'; -import { EditorExtensions, EditorInput } from 'vs/workbench/common/editor'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { EditorExtensions } from 'vs/workbench/common/editor'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IEditorRegistry, EditorDescriptor } from 'vs/workbench/browser/editor'; import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor'; @@ -16,9 +16,9 @@ Registry.as(EditorExtensions.Editors).registerEditor( EditorDescriptor.create( TextFileEditor, TextFileEditor.ID, - nls.localize('textFileEditor', "Text File Editor") + localize('textFileEditor', "Text File Editor") ), [ - new SyncDescriptor(FileEditorInput) + new SyncDescriptor(FileEditorInput) ] ); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 71e8b72453..f1b1944a49 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -8,7 +8,7 @@ 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, VIEW_ID, VIEWLET_ID } from 'vs/workbench/contrib/files/common/files'; +import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext, ExplorerResourceAvailableEditorIdsContext, VIEW_ID, VIEWLET_ID, ExplorerResourceNotReadonlyContext } 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'; @@ -55,6 +55,7 @@ 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'; +import { IEditorOverrideService } from 'vs/workbench/services/editor/common/editorOverrideService'; interface IExplorerViewColors extends IColorMapping { listDropBackground?: ColorValue | undefined; @@ -169,6 +170,7 @@ export class ExplorerView extends ViewPane { @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IProgressService private readonly progressService: IProgressService, @IEditorService private readonly editorService: IEditorService, + @IEditorOverrideService private readonly editorOverrideService: IEditorOverrideService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IKeybindingService keybindingService: IKeybindingService, @IContextKeyService contextKeyService: IContextKeyService, @@ -485,16 +487,17 @@ export class ExplorerView extends ViewPane { } private setContextKeys(stat: ExplorerItem | null | undefined): void { - const isSingleFolder = this.contextService.getWorkbenchState() === WorkbenchState.FOLDER; - const resource = stat ? stat.resource : isSingleFolder ? this.contextService.getWorkspace().folders[0].uri : null; + const folders = this.contextService.getWorkspace().folders; + const resource = stat ? stat.resource : folders[folders.length - 1].uri; + stat = stat || this.explorerService.findClosest(resource); this.resourceContext.set(resource); - this.folderContext.set((isSingleFolder && !stat) || !!stat && stat.isDirectory); + this.folderContext.set(!!stat && stat.isDirectory); this.readonlyContext.set(!!stat && stat.isReadonly); - this.rootContext.set(!stat || (stat && stat.isRoot)); + this.rootContext.set(!!stat && stat.isRoot); if (resource) { - const overrides = resource ? this.editorService.getEditorOverrides(resource, undefined, undefined) : []; - this.availableEditorIdsContext.set(overrides.map(([, entry]) => entry.id).join(',')); + const overrides = resource ? this.editorOverrideService.getEditorIds(resource) : []; + this.availableEditorIdsContext.set(overrides.join(',')); } else { this.availableEditorIdsContext.reset(); } @@ -863,6 +866,7 @@ registerAction2(class extends Action2 { title: nls.localize('createNewFile', "New File"), f1: false, icon: Codicon.newFile, + precondition: ExplorerResourceNotReadonlyContext, menu: { id: MenuId.ViewTitle, group: 'navigation', @@ -885,6 +889,7 @@ registerAction2(class extends Action2 { title: nls.localize('createNewFolder', "New Folder"), f1: false, icon: Codicon.newFolder, + precondition: ExplorerResourceNotReadonlyContext, menu: { id: MenuId.ViewTitle, group: 'navigation', diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index b38f8a9597..7ead07ec16 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -7,9 +7,9 @@ import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import * as DOM from 'vs/base/browser/dom'; import * as glob from 'vs/base/common/glob'; import { IListVirtualDelegate, ListDragOverEffect } from 'vs/base/browser/ui/list/list'; -import { IProgressService, ProgressLocation, IProgressStep, IProgress } from 'vs/platform/progress/common/progress'; +import { IProgressService, ProgressLocation, } from 'vs/platform/progress/common/progress'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { IFileService, FileKind, FileOperationError, FileOperationResult, FileSystemProviderCapabilities, ByteSize } from 'vs/platform/files/common/files'; +import { IFileService, FileKind, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IDisposable, Disposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -19,8 +19,8 @@ import { ITreeNode, ITreeFilter, TreeVisibility, IAsyncDataSource, ITreeSorter, 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 } from 'vs/base/common/resources'; +import { IFilesConfiguration } from 'vs/workbench/contrib/files/common/files'; +import { dirname, joinPath, 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'; @@ -29,18 +29,16 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { equals, deepClone } from 'vs/base/common/objects'; import * as path from 'vs/base/common/path'; import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; -import { compareFileNamesDefault, compareFileExtensionsDefault } from 'vs/base/common/comparers'; -import { fillResourceDataTransfers, CodeDataTransfers, extractResources, containsDragType } from 'vs/workbench/browser/dnd'; +import { compareFileExtensionsDefault, compareFileNamesDefault, compareFileNamesUpper, compareFileExtensionsUpper, compareFileNamesLower, compareFileExtensionsLower, compareFileNamesUnicode, compareFileExtensionsUnicode } from 'vs/base/common/comparers'; +import { fillEditorsDragData, CodeDataTransfers, containsDragType } from 'vs/workbench/browser/dnd'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd'; import { Schemas } from 'vs/base/common/network'; import { NativeDragAndDropData, ExternalElementsDragAndDropData, ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { isMacintosh, isWeb } from 'vs/base/common/platform'; -import { IDialogService, IConfirmation, getFileNamesMessage } from 'vs/platform/dialogs/common/dialogs'; -import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IDialogService, getFileNamesMessage } from 'vs/platform/dialogs/common/dialogs'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; import { URI } from 'vs/base/common/uri'; -import { RunOnceWorker } from 'vs/base/common/async'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; import { findValidPasteFileTarget } from 'vs/workbench/contrib/files/browser/fileActions'; @@ -49,16 +47,16 @@ import { Emitter, Event, EventMultiplexer } from 'vs/base/common/event'; 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 { VSBuffer, newWriteableBufferStream } from 'vs/base/common/buffer'; import { ILabelService } from 'vs/platform/label/common/label'; import { isNumber } from 'vs/base/common/types'; import { domEvent } from 'vs/base/browser/event'; import { IEditableData } from 'vs/workbench/common/views'; import { IEditorInput } from 'vs/workbench/common/editor'; -import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; +import { BrowserFileUpload, NativeFileImport, getMultipleFilesOverwriteConfirm } from 'vs/workbench/contrib/files/browser/fileImportExport'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; export class ExplorerDelegate implements IListVirtualDelegate { @@ -94,7 +92,7 @@ export class ExplorerDataSource implements IAsyncDataSource { if (element instanceof ExplorerItem && element.isRoot) { @@ -669,7 +667,29 @@ export class FileSorter implements ITreeSorter { return 1; } - const sortOrder = this.explorerService.sortOrder; + const sortOrder = this.explorerService.sortOrderConfiguration.sortOrder; + const lexicographicOptions = this.explorerService.sortOrderConfiguration.lexicographicOptions; + + let compareFileNames; + let compareFileExtensions; + switch (lexicographicOptions) { + case 'upper': + compareFileNames = compareFileNamesUpper; + compareFileExtensions = compareFileExtensionsUpper; + break; + case 'lower': + compareFileNames = compareFileNamesLower; + compareFileExtensions = compareFileExtensionsLower; + break; + case 'unicode': + compareFileNames = compareFileNamesUnicode; + compareFileExtensions = compareFileExtensionsUnicode; + break; + default: + // 'default' + compareFileNames = compareFileNamesDefault; + compareFileExtensions = compareFileExtensionsDefault; + } // Sort Directories switch (sortOrder) { @@ -683,7 +703,7 @@ export class FileSorter implements ITreeSorter { } if (statA.isDirectory && statB.isDirectory) { - return compareFileNamesDefault(statA.name, statB.name); + return compareFileNames(statA.name, statB.name); } break; @@ -717,74 +737,21 @@ export class FileSorter implements ITreeSorter { // Sort Files switch (sortOrder) { case 'type': - return compareFileExtensionsDefault(statA.name, statB.name); + return compareFileExtensions(statA.name, statB.name); case 'modified': if (statA.mtime !== statB.mtime) { return (statA.mtime && statB.mtime && statA.mtime < statB.mtime) ? 1 : -1; } - return compareFileNamesDefault(statA.name, statB.name); + return compareFileNames(statA.name, statB.name); default: /* 'default', 'mixed', 'filesFirst' */ - return compareFileNamesDefault(statA.name, statB.name); + return compareFileNames(statA.name, statB.name); } } } -function getFileOverwriteConfirm(name: string): IConfirmation { - return { - message: localize('confirmOverwrite', "A file or folder with the name '{0}' already exists in the destination folder. Do you want to replace it?", name), - detail: localize('irreversible', "This action is irreversible!"), - primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), - type: 'warning' - }; -} - -function getMultipleFilesOverwriteConfirm(files: URI[]): IConfirmation { - if (files.length > 1) { - return { - message: localize('confirmManyOverwrites', "The following {0} files and/or folders already exist in the destination folder. Do you want to replace them?", files.length), - detail: getFileNamesMessage(files) + '\n' + localize('irreversible', "This action is irreversible!"), - primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), - type: 'warning' - }; - } - - return getFileOverwriteConfirm(basename(files[0])); -} - -interface IWebkitDataTransfer { - items: IWebkitDataTransferItem[]; -} - -interface IWebkitDataTransferItem { - webkitGetAsEntry(): IWebkitDataTransferItemEntry; -} - -interface IWebkitDataTransferItemEntry { - name: string | undefined; - isFile: boolean; - isDirectory: boolean; - - file(resolve: (file: File) => void, reject: () => void): void; - createReader(): IWebkitDataTransferItemEntryReader; -} - -interface IWebkitDataTransferItemEntryReader { - readEntries(resolve: (file: IWebkitDataTransferItemEntry[]) => void, reject: () => void): void -} - -interface IUploadOperation { - startTime: number; - progressScheduler: RunOnceWorker; - - filesTotal: number; - filesUploaded: number; - - totalBytesUploaded: number; -} - export class FileDragAndDrop implements ITreeDragAndDrop { private static readonly CONFIRM_DND_SETTING_KEY = 'explorer.confirmDragAndDrop'; @@ -795,7 +762,6 @@ export class FileDragAndDrop implements ITreeDragAndDrop { private dropEnabled = false; constructor( - @INotificationService private notificationService: INotificationService, @IExplorerService private explorerService: IExplorerService, @IEditorService private editorService: IEditorService, @IDialogService private dialogService: IDialogService, @@ -803,9 +769,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { @IFileService private fileService: IFileService, @IConfigurationService private configurationService: IConfigurationService, @IInstantiationService private instantiationService: IInstantiationService, - @IHostService private hostService: IHostService, @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService, - @IProgressService private readonly progressService: IProgressService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { this.toDispose = []; @@ -867,6 +831,10 @@ export class FileDragAndDrop implements ITreeDragAndDrop { if (!containsDragType(originalEvent, DataTransfers.FILES, CodeDataTransfers.FILES, DataTransfers.RESOURCES)) { return false; } + if (isWeb && originalEvent.dataTransfer?.types.indexOf('Files') === -1) { + // DnD from vscode to web is not supported #115535. Only if we are dragging from native finder / explorer then the "Files" data transfer will be set + return false; + } } // Other-Tree DND @@ -964,7 +932,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { const items = FileDragAndDrop.getStatsFromDragAndDropData(data as ElementsDragAndDropData, originalEvent); if (items && items.length && originalEvent.dataTransfer) { // Apply some datatransfer types to allow for dragging the element outside of the application - this.instantiationService.invokeFunction(fillResourceDataTransfers, items, undefined, originalEvent); + this.instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, items, originalEvent)); // The only custom data transfer we set from the explorer is a file transfer // to be able to DND between multiple code file explorers across windows @@ -1002,358 +970,25 @@ export class FileDragAndDrop implements ITreeDragAndDrop { return; } - // Desktop DND (Import file) - if (data instanceof NativeDragAndDropData) { - const cts = new CancellationTokenSource(); - - 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); - } - } - } - // In-Explorer DND (Move/Copy file) - else { - this.handleExplorerDrop(data as ElementsDragAndDropData, resolvedTarget, originalEvent).then(undefined, e => this.notificationService.warn(e)); - } - } - - private async handleWebExternalDrop(target: ExplorerItem, originalEvent: DragEvent, progress: IProgress, token: CancellationToken): Promise { - const items = (originalEvent.dataTransfer as unknown as IWebkitDataTransfer).items; - - // Somehow the items thing is being modified at random, maybe as a security - // measure since this is a DND operation. As such, we copy the items into - // an array we own as early as possible before using it. - const entries: IWebkitDataTransferItemEntry[] = []; - for (const item of items) { - entries.push(item.webkitGetAsEntry()); - } - - const results: { isFile: boolean, resource: URI }[] = []; - const operation: IUploadOperation = { - startTime: Date.now(), - progressScheduler: new RunOnceWorker(steps => { progress.report(steps[steps.length - 1]); }, 1000), - - filesTotal: entries.length, - filesUploaded: 0, - - totalBytesUploaded: 0 - }; - - for (let entry of entries) { - if (token.isCancellationRequested) { - break; - } - - // Confirm overwrite as needed - if (target && entry.name && target.getChild(entry.name)) { - const { confirmed } = await this.dialogService.confirm(getFileOverwriteConfirm(entry.name)); - if (!confirmed) { - continue; - } - - await this.explorerService.applyBulkEdit([new ResourceFileEdit(joinPath(target.resource, entry.name), undefined, { recursive: true })], { - undoLabel: localize('overwrite', "Overwrite {0}", entry.name), - progressLabel: localize('overwriting', "Overwriting {0}", entry.name), - }); - - if (token.isCancellationRequested) { - break; - } - } - - // Upload entry - const result = await this.doUploadWebFileEntry(entry, target.resource, target, progress, operation, token); - if (result) { - results.push(result); - } - } - - operation.progressScheduler.dispose(); - - // Open uploaded file in editor only if we upload just one - const firstUploadedFile = results[0]; - if (!token.isCancellationRequested && firstUploadedFile?.isFile) { - await this.editorService.openEditor({ resource: firstUploadedFile.resource, options: { pinned: true } }); - } - } - - private async doUploadWebFileEntry(entry: IWebkitDataTransferItemEntry, parentResource: URI, target: ExplorerItem | undefined, progress: IProgress, operation: IUploadOperation, token: CancellationToken): Promise<{ isFile: boolean, resource: URI } | undefined> { - if (token.isCancellationRequested || !entry.name || (!entry.isFile && !entry.isDirectory)) { - return undefined; - } - - // Report progress - let fileBytesUploaded = 0; - const reportProgress = (fileSize: number, bytesUploaded: number): void => { - fileBytesUploaded += bytesUploaded; - operation.totalBytesUploaded += bytesUploaded; - - const bytesUploadedPerSecond = operation.totalBytesUploaded / ((Date.now() - operation.startTime) / 1000); - - // Small file - let message: string; - if (fileSize < ByteSize.MB) { - if (operation.filesTotal === 1) { - message = `${entry.name}`; - } else { - message = localize('uploadProgressSmallMany', "{0} of {1} files ({2}/s)", operation.filesUploaded, operation.filesTotal, ByteSize.formatSize(bytesUploadedPerSecond)); - } - } - - // Large file - else { - message = localize('uploadProgressLarge', "{0} ({1} of {2}, {3}/s)", entry.name, ByteSize.formatSize(fileBytesUploaded), ByteSize.formatSize(fileSize), ByteSize.formatSize(bytesUploadedPerSecond)); - } - - // Report progress but limit to update only once per second - operation.progressScheduler.work({ message }); - }; - operation.filesUploaded++; - reportProgress(0, 0); - - // Handle file upload - const resource = joinPath(parentResource, entry.name); - if (entry.isFile) { - const file = await new Promise((resolve, reject) => entry.file(resolve, reject)); - - if (token.isCancellationRequested) { - return undefined; - } - - // Chrome/Edge/Firefox support stream method, but only use it for - // larger files to reduce the overhead of the streaming approach - if (typeof file.stream === 'function' && file.size > ByteSize.MB) { - await this.doUploadWebFileEntryBuffered(resource, file, reportProgress, token); - } - - // Fallback to unbuffered upload for other browsers or small files - else { - await this.doUploadWebFileEntryUnbuffered(resource, file, reportProgress); - } - - return { isFile: true, resource }; - } - - // Handle folder upload - else { - - // Create target folder - await this.fileService.createFolder(resource); - - if (token.isCancellationRequested) { - return undefined; - } - - // Recursive upload files in this directory - const dirReader = entry.createReader(); - const childEntries: IWebkitDataTransferItemEntry[] = []; - let done = false; - do { - const childEntriesChunk = await new Promise((resolve, reject) => dirReader.readEntries(resolve, reject)); - if (childEntriesChunk.length > 0) { - childEntries.push(...childEntriesChunk); - } else { - done = true; // an empty array is a signal that all entries have been read - } - } while (!done && !token.isCancellationRequested); - - // Update operation total based on new counts - operation.filesTotal += childEntries.length; - - // Upload all entries as files to target - const folderTarget = target && target.getChild(entry.name) || undefined; - for (let childEntry of childEntries) { - await this.doUploadWebFileEntry(childEntry, resource, folderTarget, progress, operation, token); - } - - return { isFile: false, resource }; - } - } - - private async doUploadWebFileEntryBuffered(resource: URI, file: File, progressReporter: (fileSize: number, bytesUploaded: number) => void, token: CancellationToken): Promise { - const writeableStream = newWriteableBufferStream({ - // Set a highWaterMark to prevent the stream - // for file upload to produce large buffers - // in-memory - highWaterMark: 10 - }); - const writeFilePromise = this.fileService.writeFile(resource, writeableStream); - - // Read the file in chunks using File.stream() web APIs try { - const reader: ReadableStreamDefaultReader = file.stream().getReader(); - let res = await reader.read(); - while (!res.done) { - if (token.isCancellationRequested) { - return undefined; + // Desktop DND (Import file) + if (data instanceof NativeDragAndDropData) { + if (isWeb) { + const browserUpload = this.instantiationService.createInstance(BrowserFileUpload); + await browserUpload.upload(target, originalEvent); + } else { + const nativeImport = this.instantiationService.createInstance(NativeFileImport); + await nativeImport.import(resolvedTarget, originalEvent); } - - // Write buffer into stream but make sure to wait - // in case the highWaterMark is reached - const buffer = VSBuffer.wrap(res.value); - await writeableStream.write(buffer); - - if (token.isCancellationRequested) { - return undefined; - } - - // Report progress - progressReporter(file.size, buffer.byteLength); - - res = await reader.read(); } - writeableStream.end(undefined); + + // In-Explorer DND (Move/Copy file) + else { + await this.handleExplorerDrop(data as ElementsDragAndDropData, resolvedTarget, originalEvent); + } } catch (error) { - writeableStream.error(error); - writeableStream.end(); - } - - if (token.isCancellationRequested) { - return undefined; - } - - // Wait for file being written to target - await writeFilePromise; - } - - private doUploadWebFileEntryUnbuffered(resource: URI, file: File, progressReporter: (fileSize: number, bytesUploaded: number) => void): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = async event => { - try { - if (event.target?.result instanceof ArrayBuffer) { - const buffer = VSBuffer.wrap(new Uint8Array(event.target.result)); - await this.fileService.writeFile(resource, buffer); - - // Report progress - progressReporter(file.size, buffer.byteLength); - } else { - throw new Error('Could not read from dropped file.'); - } - - resolve(); - } catch (error) { - reject(error); - } - }; - - // Start reading the file to trigger `onload` - reader.readAsArrayBuffer(file); - }); - } - - private async handleExternalDrop(target: ExplorerItem, originalEvent: DragEvent, token: CancellationToken): Promise { - - // Check for dropped external files to be folders - const droppedResources = extractResources(originalEvent, true); - const result = await this.fileService.resolveAll(droppedResources.map(droppedResource => ({ resource: droppedResource.resource }))); - - if (token.isCancellationRequested) { - return; - } - - // Pass focus to window - this.hostService.focus(); - - // Handle folders by adding to workspace if we are in workspace context and if dropped on top - const folders = result.filter(r => r.success && r.stat && r.stat.isDirectory).map(result => ({ uri: result.stat!.resource })); - if (folders.length > 0 && target.isRoot) { - const buttons = [ - folders.length > 1 ? localize('copyFolders', "&&Copy Folders") : localize('copyFolder', "&&Copy Folder"), - localize('cancel', "Cancel") - ]; - const workspaceFolderSchemas = this.contextService.getWorkspace().folders.map(f => f.uri.scheme); - let message = folders.length > 1 ? localize('copyfolders', "Are you sure to want to copy folders?") : localize('copyfolder', "Are you sure to want to copy '{0}'?", basename(folders[0].uri)); - if (folders.some(f => workspaceFolderSchemas.indexOf(f.uri.scheme) >= 0)) { - // We only allow to add a folder to the workspace if there is already a workspace folder with that scheme - buttons.unshift(folders.length > 1 ? localize('addFolders', "&&Add Folders to Workspace") : localize('addFolder', "&&Add Folder to Workspace")); - message = folders.length > 1 ? localize('dropFolders', "Do you want to copy the folders or add the folders to the workspace?") - : localize('dropFolder', "Do you want to copy '{0}' or add '{0}' as a folder to the workspace?", basename(folders[0].uri)); - } - - const { choice } = await this.dialogService.show(Severity.Info, message, buttons); - if (choice === buttons.length - 3) { - return this.workspaceEditingService.addFolders(folders); - } - if (choice === buttons.length - 2) { - return this.addResources(target, droppedResources.map(res => res.resource), token); - } - - return undefined; - } - - // Handle dropped files (only support FileStat as target) - else if (target instanceof ExplorerItem) { - return this.addResources(target, droppedResources.map(res => res.resource), token); - } - } - - private async addResources(target: ExplorerItem, resources: URI[], token: CancellationToken): Promise { - if (resources && resources.length > 0) { - - // Resolve target to check for name collisions and ask user - const targetStat = await this.fileService.resolve(target.resource); - - if (token.isCancellationRequested) { - return; - } - - // Check for name collisions - const targetNames = new Set(); - const caseSensitive = this.fileService.hasCapability(target.resource, FileSystemProviderCapabilities.PathCaseSensitive); - if (targetStat.children) { - targetStat.children.forEach(child => { - targetNames.add(caseSensitive ? child.name : child.name.toLowerCase()); - }); - } - - const resourcesFiltered = (await Promise.all(resources.map(async resource => { - if (targetNames.has(caseSensitive ? basename(resource) : basename(resource).toLowerCase())) { - const confirmationResult = await this.dialogService.confirm(getFileOverwriteConfirm(basename(resource))); - if (!confirmationResult.confirmed) { - return undefined; - } - } - return resource; - }))).filter(r => r instanceof URI) as URI[]; - const resourceFileEdits = resourcesFiltered.map(resource => { - const sourceFileName = basename(resource); - const targetFile = joinPath(target.resource, sourceFileName); - return new ResourceFileEdit(resource, targetFile, { overwrite: true, copy: true }); - }); - - await this.explorerService.applyBulkEdit(resourceFileEdits, { - 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 && !item.isDirectory) { - this.editorService.openEditor({ resource: item.resource, options: { pinned: true } }); - } - } + this.dialogService.show(Severity.Error, toErrorMessage(error)); } } @@ -1395,15 +1030,15 @@ export class FileDragAndDrop implements ITreeDragAndDrop { const sources = items.filter(s => !s.isRoot); if (isCopy) { - await this.doHandleExplorerDropOnCopy(sources, target); - } else { - return this.doHandleExplorerDropOnMove(sources, target); + return this.doHandleExplorerDropOnCopy(sources, target); } + + return this.doHandleExplorerDropOnMove(sources, target); } - private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem): Promise { + private async doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem): Promise { if (roots.length === 0) { - return Promise.resolve(undefined); + return; } const folders = this.contextService.getWorkspace().folders; @@ -1431,10 +1066,12 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } workspaceCreationData.splice(targetIndex, 0, ...rootsToMove); + return this.workspaceEditingService.updateFolders(0, workspaceCreationData.length, workspaceCreationData); } private async doHandleExplorerDropOnCopy(sources: ExplorerItem[], target: ExplorerItem): Promise { + // 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 }))); @@ -1465,6 +1102,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { try { await this.explorerService.applyBulkEdit(resourceFileEdits, options); } catch (error) { + // Conflict if ((error).fileOperationResult === FileOperationResult.FILE_MOVE_CONFLICT) { @@ -1475,20 +1113,17 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } } - const confirm = getMultipleFilesOverwriteConfirm(overwrites); // Move with overwrite if the user confirms + const confirm = getMultipleFilesOverwriteConfirm(overwrites); const { confirmed } = await this.dialogService.confirm(confirm); if (confirmed) { - try { - await this.explorerService.applyBulkEdit(resourceFileEdits.map(re => new ResourceFileEdit(re.oldResource, re.newResource, { overwrite: true })), options); - } catch (error) { - this.notificationService.error(error); - } + await this.explorerService.applyBulkEdit(resourceFileEdits.map(re => new ResourceFileEdit(re.oldResource, re.newResource, { overwrite: true })), options); } } - // Any other error + + // Any other error: bubble up else { - this.notificationService.error(error); + throw error; } } } diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 930eaf8913..4a217869aa 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -13,7 +13,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati 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 { IEditorInput, Verbosity, EditorResourceAccessor, SideBySideEditor, EditorInputCapabilities, IEditorIdentifier } from 'vs/workbench/common/editor'; 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'; @@ -32,13 +32,12 @@ import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/m import { IMenuService, MenuId, IMenu, Action2, registerAction2, MenuRegistry } from 'vs/platform/actions/common/actions'; import { OpenEditorsDirtyEditorContext, OpenEditorsGroupContext, OpenEditorsReadonlyEditorContext, SAVE_ALL_LABEL, SAVE_ALL_COMMAND_ID, NEW_UNTITLED_FILE_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 { ResourcesDropHandler, fillEditorsDragData, CodeDataTransfers, containsDragType } from 'vs/workbench/browser/dnd'; 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'; import { ElementsDragAndDropData, NativeDragAndDropData } from 'vs/base/browser/ui/list/listView'; -import { URI } from 'vs/base/common/uri'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { isWeb } from 'vs/base/common/platform'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; @@ -164,6 +163,7 @@ export class OpenEditorsView extends ViewPane { } case GroupChangeKind.EDITOR_DIRTY: case GroupChangeKind.EDITOR_LABEL: + case GroupChangeKind.EDITOR_CAPABILITIES: case GroupChangeKind.EDITOR_STICKY: case GroupChangeKind.EDITOR_PIN: { this.list.splice(index, 1, [new OpenEditor(e.editor!, group)]); @@ -269,7 +269,7 @@ export class OpenEditorsView extends ViewPane { if (element instanceof OpenEditor) { const resource = element.getResource(); this.dirtyEditorFocusedContext.set(element.editor.isDirty() && !element.editor.isSaving()); - this.readonlyEditorFocusedContext.set(element.editor.isReadonly()); + this.readonlyEditorFocusedContext.set(element.editor.hasCapability(EditorInputCapabilities.Readonly)); this.resourceContext.set(withUndefinedAsNull(resource)); } else if (!!element) { this.groupFocusedContext.set(true); @@ -653,21 +653,18 @@ class OpenEditorsDragAndDrop implements IListDragAndDrop).elements; - const resources: URI[] = []; + const editors: IEditorIdentifier[] = []; if (items) { - items.forEach(i => { - if (i instanceof OpenEditor) { - const resource = i.getResource(); - if (resource) { - resources.push(resource); - } + for (const item of items) { + if (item instanceof OpenEditor) { + editors.push(item); } - }); + } } - if (resources.length) { + if (editors.length) { // Apply some datatransfer types to allow for dragging the element outside of the application - this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, undefined, originalEvent); + this.instantiationService.invokeFunction(fillEditorsDragData, editors, originalEvent); } } diff --git a/src/vs/workbench/contrib/files/common/explorerModel.ts b/src/vs/workbench/contrib/files/common/explorerModel.ts index bf6f7baae1..397d605d54 100644 --- a/src/vs/workbench/contrib/files/common/explorerModel.ts +++ b/src/vs/workbench/contrib/files/common/explorerModel.ts @@ -30,7 +30,7 @@ export class ExplorerModel implements IDisposable { fileService: IFileService ) { const setRoots = () => this._roots = this.contextService.getWorkspace().folders - .map(folder => new ExplorerItem(folder.uri, fileService, undefined, true, false, folder.name)); + .map(folder => new ExplorerItem(folder.uri, fileService, undefined, true, false, false, folder.name)); setRoots(); this._listener = this.contextService.onDidChangeWorkspaceFolders(() => { @@ -89,6 +89,7 @@ export class ExplorerItem { private _parent: ExplorerItem | undefined, private _isDirectory?: boolean, private _isSymbolicLink?: boolean, + private _readonly?: boolean, private _name: string = basenameOrAuthority(resource), private _mtime?: number, private _unknown = false @@ -124,7 +125,7 @@ export class ExplorerItem { } get isReadonly(): boolean { - return this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); + return this._readonly || this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); } get mtime(): number | undefined { @@ -179,7 +180,7 @@ export class ExplorerItem { } static create(fileService: IFileService, raw: IFileStat, parent: ExplorerItem | undefined, resolveTo?: readonly URI[]): ExplorerItem { - const stat = new ExplorerItem(raw.resource, fileService, parent, raw.isDirectory, raw.isSymbolicLink, raw.name, raw.mtime, !raw.isFile && !raw.isDirectory); + const stat = new ExplorerItem(raw.resource, fileService, parent, raw.isDirectory, raw.isSymbolicLink, raw.readonly, raw.name, raw.mtime, !raw.isFile && !raw.isDirectory); // Recursively add children if present if (stat.isDirectory) { diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index 56001c4574..73bcc0c435 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -83,6 +83,7 @@ export interface IFilesConfiguration extends PlatformIFilesConfiguration, IWorkb enableDragAndDrop: boolean; confirmDelete: boolean; sortOrder: SortOrder; + sortOrderLexicographicOptions: LexicographicOptions; decorations: { colors: boolean; badges: boolean; @@ -105,6 +106,18 @@ export const enum SortOrder { Modified = 'modified' } +export const enum LexicographicOptions { + Default = 'default', + Upper = 'upper', + Lower = 'lower', + Unicode = 'unicode', +} + +export interface ISortOrderConfiguration { + sortOrder: SortOrder; + lexicographicOptions: LexicographicOptions; +} + export class TextFileContentProvider extends Disposable implements ITextModelContentProvider { private readonly fileWatcherDisposable = this._register(new MutableDisposable()); @@ -119,8 +132,8 @@ export class TextFileContentProvider extends Disposable implements ITextModelCon static async open(resource: URI, scheme: string, label: string, editorService: IEditorService, options?: ITextEditorOptions): Promise { await editorService.openEditor({ - leftResource: TextFileContentProvider.resourceToTextFile(scheme, resource), - rightResource: resource, + originalInput: { resource: TextFileContentProvider.resourceToTextFile(scheme, resource) }, + modifiedInput: { resource }, label, options }); diff --git a/src/vs/workbench/contrib/files/electron-sandbox/files.contribution.ts b/src/vs/workbench/contrib/files/electron-sandbox/files.contribution.ts index e3549c02a8..82727a9992 100644 --- a/src/vs/workbench/contrib/files/electron-sandbox/files.contribution.ts +++ b/src/vs/workbench/contrib/files/electron-sandbox/files.contribution.ts @@ -5,8 +5,8 @@ import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -import { EditorExtensions, EditorInput } from 'vs/workbench/common/editor'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { EditorExtensions } from 'vs/workbench/common/editor'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IEditorRegistry, EditorDescriptor } from 'vs/workbench/browser/editor'; import { NativeTextFileEditor } from 'vs/workbench/contrib/files/electron-sandbox/textFileEditor'; @@ -19,6 +19,6 @@ Registry.as(EditorExtensions.Editors).registerEditor( nls.localize('textFileEditor', "Text File Editor") ), [ - new SyncDescriptor(FileEditorInput) + new SyncDescriptor(FileEditorInput) ] ); diff --git a/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts b/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts index 5376af2973..131e0e109c 100644 --- a/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts @@ -5,8 +5,7 @@ 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 { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; 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/errors'; import { toAction } from 'vs/base/common/actions'; @@ -25,6 +24,7 @@ import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; import { IProductService } from 'vs/platform/product/common/productService'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; /** * An implementation of editor for file system resources. @@ -52,7 +52,7 @@ export class NativeTextFileEditor extends TextFileEditor { super(telemetryService, fileService, viewletService, instantiationService, contextService, storageService, textResourceConfigurationService, editorService, themeService, editorGroupService, textFileService, explorerService, uriIdentityService); } - protected override handleSetInputError(error: Error, input: FileEditorInput, options: EditorOptions | undefined): void { + protected override handleSetInputError(error: Error, input: FileEditorInput, options: ITextEditorOptions | undefined): void { // Allow to restart with higher memory limit if the file is too large if ((error).fileOperationResult === FileOperationResult.FILE_EXCEEDS_MEMORY_LIMIT) { diff --git a/src/vs/workbench/contrib/files/test/browser/explorerModel.test.ts b/src/vs/workbench/contrib/files/test/browser/explorerModel.test.ts index 6031c65887..ccb5e87fa4 100644 --- a/src/vs/workbench/contrib/files/test/browser/explorerModel.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/explorerModel.test.ts @@ -14,7 +14,7 @@ import { TestFileService } from 'vs/workbench/test/browser/workbenchTestServices const fileService = new TestFileService(); function createStat(this: any, path: string, name: string, isFolder: boolean, hasChildren: boolean, size: number, mtime: number): ExplorerItem { - return new ExplorerItem(toResource.call(this, path), fileService, undefined, isFolder, false, name, mtime); + return new ExplorerItem(toResource.call(this, path), fileService, undefined, isFolder, false, false, name, mtime); } suite('Files - View Model', function () { @@ -245,19 +245,19 @@ suite('Files - View Model', function () { }); test('Merge Local with Disk', function () { - const merge1 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), fileService, undefined, true, false, 'to', Date.now()); - const merge2 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), fileService, undefined, true, false, 'to', Date.now()); + const merge1 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), fileService, undefined, true, false, false, 'to', Date.now()); + const merge2 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), fileService, undefined, true, false, false, 'to', Date.now()); // Merge Properties ExplorerItem.mergeLocalWithDisk(merge2, merge1); assert.strictEqual(merge1.mtime, merge2.mtime); // Merge Child when isDirectoryResolved=false is a no-op - merge2.addChild(new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), fileService, undefined, true, false, 'foo.html', Date.now())); + merge2.addChild(new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), fileService, undefined, true, false, false, 'foo.html', Date.now())); ExplorerItem.mergeLocalWithDisk(merge2, merge1); // Merge Child with isDirectoryResolved=true - const child = new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), fileService, undefined, true, false, 'foo.html', Date.now()); + const child = new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), fileService, undefined, true, false, false, 'foo.html', Date.now()); merge2.removeChild(child); merge2.addChild(child); (merge2)._isDirectoryResolved = true; diff --git a/src/vs/workbench/contrib/files/test/browser/explorerView.test.ts b/src/vs/workbench/contrib/files/test/browser/explorerView.test.ts index 19737e6fe0..97da3eb429 100644 --- a/src/vs/workbench/contrib/files/test/browser/explorerView.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/explorerView.test.ts @@ -19,7 +19,7 @@ const $ = dom.$; const fileService = new TestFileService(); function createStat(this: any, path: string, name: string, isFolder: boolean, hasChildren: boolean, size: number, mtime: number, isSymLink = false, isUnknown = false): ExplorerItem { - return new ExplorerItem(toResource.call(this, path), fileService, undefined, isFolder, isSymLink, name, mtime, isUnknown); + return new ExplorerItem(toResource.call(this, path), fileService, undefined, isFolder, isSymLink, false, name, mtime, isUnknown); } suite('Files - ExplorerView', () => { 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 6a308654b1..8105205e84 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -6,12 +6,12 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { toResource } from 'vs/base/test/common/utils'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { workbenchInstantiationService, TestServiceAccessor, TestEditorService, getLastResolvedFileStat } from 'vs/workbench/test/browser/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorInputFactoryRegistry, Verbosity, EditorExtensions } from 'vs/workbench/common/editor'; +import { IEditorInputFactoryRegistry, Verbosity, EditorExtensions, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { EncodingMode, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; -import { FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; +import { FileOperationResult, FileOperationError, NotModifiedSinceFileOperationError, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { timeout } from 'vs/base/common/async'; import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; @@ -19,15 +19,16 @@ 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 { FileEditorInputSerializer } from 'vs/workbench/contrib/files/browser/files'; +import { FileEditorInputSerializer } from 'vs/workbench/contrib/files/browser/editors/fileEditorHandler'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; suite('Files - FileEditorInput', () => { let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; - function createFileInput(resource: URI, preferredResource?: URI, preferredMode?: string, preferredName?: string, preferredDescription?: string): FileEditorInput { - return instantiationService.createInstance(FileEditorInput, resource, preferredResource, preferredName, preferredDescription, undefined, preferredMode); + function createFileInput(resource: URI, preferredResource?: URI, preferredMode?: string, preferredName?: string, preferredDescription?: string, preferredContents?: string): FileEditorInput { + return instantiationService.createInstance(FileEditorInput, resource, preferredResource, preferredName, preferredDescription, undefined, preferredMode, preferredContents); } setup(() => { @@ -57,6 +58,14 @@ suite('Files - FileEditorInput', () => { assert.ok(input.getDescription()); assert.ok(input.getTitle(Verbosity.SHORT)); + assert.ok(!input.hasCapability(EditorInputCapabilities.Untitled)); + assert.ok(!input.hasCapability(EditorInputCapabilities.Readonly)); + assert.ok(!input.hasCapability(EditorInputCapabilities.Singleton)); + assert.ok(!input.hasCapability(EditorInputCapabilities.RequiresTrust)); + + const untypedInput = input.asResourceEditorInput(0); + assert.strictEqual(untypedInput.resource.toString(), input.resource.toString()); + assert.strictEqual('file.js', input.getName()); assert.strictEqual(toResource.call(this, '/foo/bar/file.js').fsPath, input.resource.fsPath); @@ -99,6 +108,30 @@ suite('Files - FileEditorInput', () => { } }); + test('reports as untitled without supported file scheme', async function () { + let input = createFileInput(toResource.call(this, '/foo/bar/file.js').with({ scheme: 'someTestingScheme' })); + + assert.ok(input.hasCapability(EditorInputCapabilities.Untitled)); + assert.ok(!input.hasCapability(EditorInputCapabilities.Readonly)); + }); + + test('reports as readonly with readonly file scheme', async function () { + + class ReadonlyInMemoryFileSystemProvider extends InMemoryFileSystemProvider { + override readonly capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.Readonly; + } + + const disposable = accessor.fileService.registerProvider('someTestingReadonlyScheme', new ReadonlyInMemoryFileSystemProvider()); + try { + let input = createFileInput(toResource.call(this, '/foo/bar/file.js').with({ scheme: 'someTestingReadonlyScheme' })); + + assert.ok(!input.hasCapability(EditorInputCapabilities.Untitled)); + assert.ok(input.hasCapability(EditorInputCapabilities.Readonly)); + } finally { + disposable.dispose(); + } + }); + test('preferred resource', function () { const resource = toResource.call(this, '/foo/bar/updatefile.js'); const preferredResource = toResource.call(this, '/foo/bar/UPDATEFILE.js'); @@ -153,6 +186,32 @@ suite('Files - FileEditorInput', () => { assert.strictEqual(model2.textEditorModel!.getModeId(), mode); }); + test('preferred contents', async function () { + const input = createFileInput(toResource.call(this, '/foo/bar/file.js'), undefined, undefined, undefined, undefined, 'My contents'); + + const model = await input.resolve() as TextFileEditorModel; + assert.strictEqual(model.textEditorModel!.getValue(), 'My contents'); + assert.strictEqual(input.isDirty(), true); + + const untypedInput = input.asResourceEditorInput(0); + assert.strictEqual(untypedInput.contents, 'My contents'); + + input.setPreferredContents('Other contents'); + await input.resolve(); + assert.strictEqual(model.textEditorModel!.getValue(), 'Other contents'); + + model.textEditorModel?.setValue('Changed contents'); + await input.resolve(); + assert.strictEqual(model.textEditorModel!.getValue(), 'Changed contents'); // preferred contents only used once + + const input2 = createFileInput(toResource.call(this, '/foo/bar/file.js')); + input2.setPreferredContents('My contents'); + + const model2 = await input2.resolve() as TextFileEditorModel; + assert.strictEqual(model2.textEditorModel!.getValue(), 'My contents'); + assert.strictEqual(input2.isDirty(), true); + }); + test('matches', function () { const input1 = createFileInput(toResource.call(this, '/foo/bar/updatefile.js')); const input2 = createFileInput(toResource.call(this, '/foo/bar/updatefile.js')); @@ -342,4 +401,45 @@ suite('Files - FileEditorInput', () => { fileInput.dispose(); }); + + test('reports readonly changes', async function () { + const input = createFileInput(toResource.call(this, '/foo/bar/updatefile.js')); + + let listenerCount = 0; + const listener = input.onDidChangeCapabilities(() => { + listenerCount++; + }); + + const model = await accessor.textFileService.files.resolve(input.resource); + + assert.strictEqual(model.isReadonly(), false); + assert.strictEqual(input.hasCapability(EditorInputCapabilities.Readonly), false); + + const stat = await accessor.fileService.resolve(input.resource, { resolveMetadata: true }); + + try { + accessor.fileService.readShouldThrowError = new NotModifiedSinceFileOperationError('file not modified since', { ...stat, readonly: true }); + await input.resolve(); + } finally { + accessor.fileService.readShouldThrowError = undefined; + } + + assert.strictEqual(model.isReadonly(), true); + assert.strictEqual(input.hasCapability(EditorInputCapabilities.Readonly), true); + assert.strictEqual(listenerCount, 1); + + try { + accessor.fileService.readShouldThrowError = new NotModifiedSinceFileOperationError('file not modified since', { ...stat, readonly: false }); + await input.resolve(); + } finally { + accessor.fileService.readShouldThrowError = undefined; + } + + assert.strictEqual(model.isReadonly(), false); + assert.strictEqual(input.hasCapability(EditorInputCapabilities.Readonly), false); + assert.strictEqual(listenerCount, 2); + + input.dispose(); + listener.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 2ad3ca7cce..fdfb2e133e 100644 --- a/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts @@ -24,8 +24,9 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { whenTextEditorClosed } from 'vs/workbench/browser/editor'; import { FILE_EDITOR_INPUT_ID } from 'vs/workbench/contrib/files/common/files'; +import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; +import { TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; suite('Files - TextFileEditorTracker', () => { @@ -58,6 +59,7 @@ suite('Files - TextFileEditorTracker', () => { const part = await createEditorPart(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, part); + instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService(false)); const editorService: EditorService = instantiationService.createInstance(EditorService); instantiationService.stub(IEditorService, editorService); @@ -144,15 +146,15 @@ suite('Files - TextFileEditorTracker', () => { test.skip('dirty untitled text file model opens as editor', async function () { // {{SQL CARBON EDIT}} tabcolormode failure const accessor = await createTracker(); - const untitledEditor = accessor.editorService.createEditorInput({ forceUntitled: true }) as UntitledTextEditorInput; - const model = disposables.add(await untitledEditor.resolve()); + const untitledTextEditor = accessor.editorService.createEditorInput({ forceUntitled: true }) as UntitledTextEditorInput; + const model = disposables.add(await untitledTextEditor.resolve()); - assert.ok(!accessor.editorService.isOpened(untitledEditor)); + assert.ok(!accessor.editorService.isOpened(untitledTextEditor)); model.textEditorModel?.setValue('Super Good'); await awaitEditorOpening(accessor.editorService); - assert.ok(accessor.editorService.isOpened(untitledEditor)); + assert.ok(accessor.editorService.isOpened(untitledTextEditor)); }); function awaitEditorOpening(editorService: IEditorService): Promise { @@ -182,26 +184,4 @@ suite('Files - TextFileEditorTracker', () => { }); }); } - - test('whenTextEditorClosed (single editor)', async function () { - return testWhenTextEditorClosed(toResource.call(this, '/path/index.txt')); - }); - - test('whenTextEditorClosed (multiple editor)', async function () { - return testWhenTextEditorClosed(toResource.call(this, '/path/index.txt'), toResource.call(this, '/test.html')); - }); - - async function testWhenTextEditorClosed(...resources: URI[]): Promise { - const accessor = await createTracker(false); - - for (const resource of resources) { - await accessor.editorService.openEditor({ resource, options: { pinned: true } }); - } - - const closedPromise = accessor.instantitionService.invokeFunction(accessor => whenTextEditorClosed(accessor, resources)); - - accessor.editorGroupService.activeGroup.closeAllEditors(); - - await closedPromise; - } }); diff --git a/src/vs/workbench/contrib/issue/browser/issue.web.contribution.ts b/src/vs/workbench/contrib/issue/browser/issue.web.contribution.ts index 30cb06aab5..22af5257e1 100644 --- a/src/vs/workbench/contrib/issue/browser/issue.web.contribution.ts +++ b/src/vs/workbench/contrib/issue/browser/issue.web.contribution.ts @@ -13,7 +13,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IWebIssueService, WebIssueService } from 'vs/workbench/contrib/issue/browser/issueService'; -import { OpenIssueReporterArgs, OpenIssueReporterActionId } from 'vs/workbench/contrib/issue/common/commands'; +import { OpenIssueReporterArgs, OpenIssueReporterActionId, OpenIssueReporterApiCommandId } from 'vs/workbench/contrib/issue/common/commands'; class RegisterIssueContribution implements IWorkbenchContribution { @@ -34,6 +34,53 @@ class RegisterIssueContribution implements IWorkbenchContribution { return accessor.get(IWebIssueService).openReporter({ extensionId }); }); + CommandsRegistry.registerCommand({ + id: OpenIssueReporterApiCommandId, + handler: function (accessor, args?: [string] | OpenIssueReporterArgs) { + let extensionId: string | undefined; + if (args) { + if (Array.isArray(args)) { + [extensionId] = args; + } else { + extensionId = args.extensionId; + } + } + + if (!!extensionId && typeof extensionId !== 'string') { + throw new Error(`Invalid argument when running '${OpenIssueReporterApiCommandId}: 'extensionId' must be of type string `); + } + + return accessor.get(IWebIssueService).openReporter({ extensionId }); + }, + description: { + description: 'Open the issue reporter and optionally prefill part of the form.', + args: [ + { + name: 'options', + description: 'Data to use to prefill the issue reporter with.', + isOptional: true, + schema: { + oneOf: [ + { + type: 'string', + description: 'The extension id to preselect.' + }, + { + type: 'object', + properties: { + extensionId: { + type: 'string' + }, + } + + } + ] + } + }, + ] + } + }); + const command: ICommandAction = { id: OpenIssueReporterActionId, title: { value: OpenIssueReporterActionLabel, original: 'Report Issue' }, diff --git a/src/vs/workbench/contrib/issue/common/commands.ts b/src/vs/workbench/contrib/issue/common/commands.ts index 94cbb9f656..9035fd1f52 100644 --- a/src/vs/workbench/contrib/issue/common/commands.ts +++ b/src/vs/workbench/contrib/issue/common/commands.ts @@ -5,6 +5,8 @@ export const OpenIssueReporterActionId = 'workbench.action.openIssueReporter'; +export const OpenIssueReporterApiCommandId = 'vscode.openIssueReporter'; + export interface OpenIssueReporterArgs { readonly extensionId?: string; readonly issueTitle?: string; diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts index 2fdb58f1ed..23efd71c34 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts @@ -3,11 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Registry } from 'vs/platform/registry/common/platform'; -import * as nls from 'vs/nls'; +import { localize } from 'vs/nls'; import product from 'vs/platform/product/common/product'; -import { SyncActionDescriptor, ICommandAction, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { IWorkbenchActionRegistry, Extensions, CATEGORIES } from 'vs/workbench/common/actions'; +import { ICommandAction, MenuRegistry, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { CATEGORIES } from 'vs/workbench/common/actions'; import { ReportPerformanceIssueUsingReporterAction, OpenProcessExplorer } from 'vs/workbench/contrib/issue/electron-sandbox/issueActions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; @@ -15,14 +14,10 @@ import { WorkbenchIssueService } from 'vs/workbench/services/issue/electron-sand import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IssueReporterData } from 'vs/platform/issue/common/issue'; import { IIssueService } from 'vs/platform/issue/electron-sandbox/issue'; -import { OpenIssueReporterArgs, OpenIssueReporterActionId } from 'vs/workbench/contrib/issue/common/commands'; - -const workbenchActionsRegistry = Registry.as(Extensions.WorkbenchActions); +import { OpenIssueReporterArgs, OpenIssueReporterActionId, OpenIssueReporterApiCommandId } from 'vs/workbench/contrib/issue/common/commands'; if (!!product.reportIssueUrl) { - workbenchActionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ReportPerformanceIssueUsingReporterAction), 'Help: Report Performance Issue', CATEGORIES.Help.value); - - const OpenIssueReporterActionLabel = nls.localize({ key: 'reportIssueInEnglish', comment: ['Translate this to "Report Issue in English" in all languages please!'] }, "Report Issue..."); + registerAction2(ReportPerformanceIssueUsingReporterAction); CommandsRegistry.registerCommand(OpenIssueReporterActionId, function (accessor, args?: [string] | OpenIssueReporterArgs) { const data: Partial = Array.isArray(args) @@ -32,16 +27,81 @@ if (!!product.reportIssueUrl) { return accessor.get(IWorkbenchIssueService).openReporter(data); }); - const command: ICommandAction = { + CommandsRegistry.registerCommand({ + id: OpenIssueReporterApiCommandId, + handler: function (accessor, args?: [string] | OpenIssueReporterArgs) { + const data: Partial = Array.isArray(args) + ? { extensionId: args[0] } + : args || {}; + + return accessor.get(IWorkbenchIssueService).openReporter(data); + }, + description: { + description: 'Open the issue reporter and optionally prefill part of the form.', + args: [ + { + name: 'options', + description: 'Data to use to prefill the issue reporter with.', + isOptional: true, + schema: { + oneOf: [ + { + type: 'string', + description: 'The extension id to preselect.' + }, + { + type: 'object', + properties: { + extensionId: { + type: 'string' + }, + issueTitle: { + type: 'string' + }, + issueBody: { + type: 'string' + } + } + + } + ] + } + }, + ] + } + }); + + const reportIssue: ICommandAction = { id: OpenIssueReporterActionId, - title: { value: OpenIssueReporterActionLabel, original: 'Report Issue' }, + title: { + value: localize({ key: 'reportIssueInEnglish', comment: ['Translate this to "Report Issue in English" in all languages please!'] }, "Report Issue..."), + original: 'Report Issue...' + }, category: CATEGORIES.Help }; - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command }); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: reportIssue }); + + MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '3_feedback', + command: { + id: OpenIssueReporterActionId, + title: localize({ key: 'miReportIssue', comment: ['&& denotes a mnemonic', 'Translate this to "Report Issue in English" in all languages please!'] }, "Report &&Issue") + }, + order: 3 + }); } -workbenchActionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(OpenProcessExplorer), 'Developer: Open Process Explorer', CATEGORIES.Developer.value); +MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '5_tools', + command: { + id: 'workbench.action.openProcessExplorer', + title: localize({ key: 'miOpenProcessExplorerer', comment: ['&& denotes a mnemonic'] }, "Open &&Process Explorer") + }, + order: 2 +}); + +registerAction2(OpenProcessExplorer); registerSingleton(IWorkbenchIssueService, WorkbenchIssueService, true); diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueActions.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueActions.ts index 8ddf907aac..1e71e177e4 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueActions.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueActions.ts @@ -3,41 +3,49 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Action } from 'vs/base/common/actions'; -import * as nls from 'vs/nls'; +import { localize } from 'vs/nls'; +import { Action2 } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IssueType } from 'vs/platform/issue/common/issue'; +import { CATEGORIES } from 'vs/workbench/common/actions'; import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; -export class OpenProcessExplorer extends Action { +export class OpenProcessExplorer extends Action2 { + static readonly ID = 'workbench.action.openProcessExplorer'; - static readonly LABEL = nls.localize('openProcessExplorer', "Open Process Explorer"); - constructor( - id: string, - label: string, - @IWorkbenchIssueService private readonly issueService: IWorkbenchIssueService - ) { - super(id, label); + constructor() { + super({ + id: OpenProcessExplorer.ID, + title: { value: localize('openProcessExplorer', "Open Process Explorer"), original: 'Open Process Explorer' }, + category: CATEGORIES.Developer, + f1: true + }); } - override run(): Promise { - return this.issueService.openProcessExplorer(); + override async run(accessor: ServicesAccessor): Promise { + const issueService = accessor.get(IWorkbenchIssueService); + + return issueService.openProcessExplorer(); } } -export class ReportPerformanceIssueUsingReporterAction extends Action { +export class ReportPerformanceIssueUsingReporterAction extends Action2 { + static readonly ID = 'workbench.action.reportPerformanceIssueUsingReporter'; - static readonly LABEL = nls.localize({ key: 'reportPerformanceIssue', comment: [`Here, 'issue' means problem or bug`] }, "Report Performance Issue"); - constructor( - id: string, - label: string, - @IWorkbenchIssueService private readonly issueService: IWorkbenchIssueService - ) { - super(id, label); + constructor() { + super({ + id: ReportPerformanceIssueUsingReporterAction.ID, + title: { value: localize({ key: 'reportPerformanceIssue', comment: [`Here, 'issue' means problem or bug`] }, "Report Performance Issue"), original: 'Report Performance Issue' }, + category: CATEGORIES.Help, + f1: true + }); } - override run(): Promise { - return this.issueService.openReporter({ issueType: IssueType.PerformanceIssue }); + override async run(accessor: ServicesAccessor): Promise { + const issueService = accessor.get(IWorkbenchIssueService); + + return issueService.openReporter({ issueType: IssueType.PerformanceIssue }); } } diff --git a/src/vs/workbench/contrib/localizations/browser/localizationsActions.ts b/src/vs/workbench/contrib/localizations/browser/localizationsActions.ts index b73a3188e7..a71aff2cb7 100644 --- a/src/vs/workbench/contrib/localizations/browser/localizationsActions.ts +++ b/src/vs/workbench/contrib/localizations/browser/localizationsActions.ts @@ -41,7 +41,7 @@ export class ConfigureLocaleAction extends Action { return availableLanguages .map(language => { return { label: language }; }) - .concat({ label: localize('installAdditionalLanguages', "Install additional languages...") }); + .concat({ label: localize('installAdditionalLanguages', "Install Additional Languages...") }); } public override async run(): Promise { diff --git a/src/vs/workbench/contrib/localizations/browser/minimalTranslations.ts b/src/vs/workbench/contrib/localizations/browser/minimalTranslations.ts index f16944b8cd..888b336bed 100644 --- a/src/vs/workbench/contrib/localizations/browser/minimalTranslations.ts +++ b/src/vs/workbench/contrib/localizations/browser/minimalTranslations.ts @@ -13,4 +13,4 @@ export const minimumTranslatedStrings: { [key: string]: string } = { searchMarketplace: localize('searchMarketplace', "Search Marketplace"), installAndRestartMessage: localize('installAndRestartMessage', "Install language pack to change the display language to {0}."), installAndRestart: localize('installAndRestart', "Install and Restart") -}; \ No newline at end of file +}; diff --git a/src/vs/workbench/contrib/markdown/common/markdownDocumentRenderer.ts b/src/vs/workbench/contrib/markdown/common/markdownDocumentRenderer.ts index f6054112f5..6a2ed64673 100644 --- a/src/vs/workbench/contrib/markdown/common/markdownDocumentRenderer.ts +++ b/src/vs/workbench/contrib/markdown/common/markdownDocumentRenderer.ts @@ -18,6 +18,10 @@ body { margin: 0 auto; } +body *:last-child { + margin-bottom: 0; +} + img { max-width: 100%; max-height: 100%; @@ -153,16 +157,17 @@ function removeEmbeddedSVGs(documentContent: string): string { 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7', 'h8', 'br', 'b', 'i', 'strong', 'em', 'a', 'pre', 'code', 'img', 'tt', 'div', 'ins', 'del', 'sup', 'sub', 'p', 'ol', 'ul', 'table', 'thead', 'tbody', 'tfoot', 'blockquote', 'dl', 'dt', 'dd', 'kbd', 'q', 'samp', 'var', 'hr', 'ruby', 'rt', 'rp', 'li', 'tr', 'td', 'th', 's', 'strike', 'summary', 'details', - 'caption', 'figure', 'figcaption', 'abbr', 'bdo', 'cite', 'dfn', 'mark', 'small', 'span', 'time', 'wbr' + 'caption', 'figure', 'figcaption', 'abbr', 'bdo', 'cite', 'dfn', 'mark', 'small', 'span', 'time', 'wbr', 'checkbox', 'checklist', 'vertically-centered' ], allowedAttributes: { '*': [ 'align', ], - img: ['src', 'alt', 'title', 'aria-label', 'width', 'height'], + img: ['src', 'alt', 'title', 'aria-label', 'width', 'height', 'centered'], span: ['class'], + checkbox: ['on-checked', 'checked-on', 'label', 'class'] }, - allowedSchemes: ['http', 'https', 'command',], + allowedSchemes: ['http', 'https', 'command'], filter(token: { tag: string, attrs: { readonly [key: string]: string } }): boolean { return token.tag !== 'svg'; } diff --git a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts index 537b92abc6..0cafac6db5 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts @@ -374,7 +374,7 @@ class MarkersStatusBarContributions extends Disposable implements IWorkbenchCont @IStatusbarService private readonly statusbarService: IStatusbarService ) { super(); - this.markersStatusItem = this._register(this.statusbarService.addEntry(this.getMarkersItem(), 'status.problems', localize('status.problems', "Problems"), StatusbarAlignment.LEFT, 50 /* Medium Priority */)); + this.markersStatusItem = this._register(this.statusbarService.addEntry(this.getMarkersItem(), 'status.problems', StatusbarAlignment.LEFT, 50 /* Medium Priority */)); this.markerService.onMarkerChanged(() => this.markersStatusItem.update(this.getMarkersItem())); } @@ -382,6 +382,7 @@ class MarkersStatusBarContributions extends Disposable implements IWorkbenchCont const markersStatistics = this.markerService.getStatistics(); const tooltip = this.getMarkersTooltip(markersStatistics); return { + name: localize('status.problems', "Problems"), text: this.getMarkersText(markersStatistics), ariaLabel: tooltip, tooltip, diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index d0ce3c8e5e..50de170847 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -32,7 +32,7 @@ import { Action, IAction } from 'vs/base/common/actions'; import { localize } from 'vs/nls'; import { IDragAndDropData } from 'vs/base/browser/dnd'; import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; -import { fillResourceDataTransfers } from 'vs/workbench/browser/dnd'; +import { fillEditorsDragData } from 'vs/workbench/browser/dnd'; import { CancelablePromise, createCancelablePromise, Delayer } from 'vs/base/common/async'; import { IModelService } from 'vs/editor/common/services/modelService'; import { Range } from 'vs/editor/common/core/range'; @@ -892,13 +892,13 @@ export class ResourceDragAndDrop implements ITreeDragAndDrop { onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void { const elements = (data as ElementsDragAndDropData).elements; - const resources: URI[] = elements + const resources = elements .filter(e => e instanceof ResourceMarkers) .map(resourceMarker => (resourceMarker as ResourceMarkers).resource); if (resources.length) { // Apply some datatransfer types to allow for dragging the element outside of the application - this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, undefined, originalEvent); + this.instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, resources, originalEvent)); } } diff --git a/src/vs/workbench/contrib/notebook/browser/constants.ts b/src/vs/workbench/contrib/notebook/browser/constants.ts deleted file mode 100644 index 97e3768827..0000000000 --- a/src/vs/workbench/contrib/notebook/browser/constants.ts +++ /dev/null @@ -1,46 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// Scrollable Element - -export const SCROLLABLE_ELEMENT_PADDING_TOP = 20; -// export const SCROLLABLE_ELEMENT_PADDING_TOP_WITH_TOOLBAR = 8; - -// Code cell layout: -// [CODE_CELL_LEFT_MARGIN][CELL_RUN_GUTTER][editorWidth][CELL_RIGHT_MARGIN] - -// Markdown cell layout: -// [CELL_MARGIN][content][CELL_RIGHT_MARGIN] - -// Markdown editor cell layout: -// [CODE_CELL_LEFT_MARGIN][content][CELL_RIGHT_MARGIN] - -// Cell sizing related -export const CELL_RIGHT_MARGIN = 16; -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 = 22; -export const CELL_STATUSBAR_HEIGHT = 22; - -// Margin above editor -export const CELL_TOP_MARGIN = 6; -export const CELL_BOTTOM_MARGIN = 6; - -export const MARKDOWN_CELL_TOP_MARGIN = 8; -export const MARKDOWN_CELL_BOTTOM_MARGIN = 8; - -// Top and bottom padding inside the monaco editor in a cell, which are included in `cell.editorHeight` -// export const EDITOR_TOP_PADDING = 12; -export const EDITOR_BOTTOM_PADDING = 4; -export const EDITOR_BOTTOM_PADDING_WITHOUT_STATUSBAR = 12; - -export const CELL_OUTPUT_PADDING = 14; - -export const COLLAPSED_INDICATOR_HEIGHT = 24; - -export const MARKDOWN_PREVIEW_PADDING = 8; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/cellOperations.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/cellOperations.ts index d3ba97f52d..065a78aa9a 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/cellOperations.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/cellOperations.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; @@ -40,6 +40,12 @@ registerAction2(class extends NotebookCellAction { primary: KeyMod.Alt | KeyCode.UpArrow, when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), weight: KeybindingWeight.WorkbenchContrib + }, + menu: { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.equals('config.notebook.dragAndDropEnabled', false), + group: CellOverflowToolbarGroups.Edit, + order: 13 } }); } @@ -60,6 +66,12 @@ registerAction2(class extends NotebookCellAction { primary: KeyMod.Alt | KeyCode.DownArrow, when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), weight: KeybindingWeight.WorkbenchContrib + }, + menu: { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.equals('config.notebook.dragAndDropEnabled', false), + group: CellOverflowToolbarGroups.Edit, + order: 14 } }); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/test/cellOperations.test.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/test/cellOperations.test.ts index 32671d6b33..20eee18672 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/test/cellOperations.test.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/test/cellOperations.test.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; @@ -17,9 +17,9 @@ suite('CellOperations', () => { test('Move cells - single cell', async function () { await withTestNotebook( [ - ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}], - ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}] ], @@ -34,9 +34,9 @@ suite('CellOperations', () => { test('Move cells - multiple cells in a selection', async function () { await withTestNotebook( [ - ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}], - ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}] ], @@ -53,9 +53,9 @@ suite('CellOperations', () => { test('Move cells - move with folding ranges', async function () { await withTestNotebook( [ - ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}], - ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}] ], @@ -80,9 +80,9 @@ suite('CellOperations', () => { test('Copy/duplicate cells - single cell', async function () { await withTestNotebook( [ - ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}], - ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}] ], @@ -99,9 +99,9 @@ suite('CellOperations', () => { test('Copy/duplicate cells - target and selection are different, #119769', async function () { await withTestNotebook( [ - ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}], - ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}] ], @@ -118,9 +118,9 @@ suite('CellOperations', () => { test('Copy/duplicate cells - multiple cells in a selection', async function () { await withTestNotebook( [ - ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}], - ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}] ], @@ -139,9 +139,9 @@ suite('CellOperations', () => { test('Copy/duplicate cells - move with folding ranges', async function () { await withTestNotebook( [ - ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}], - ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}] ], @@ -168,9 +168,9 @@ suite('CellOperations', () => { test('Join cell with below - single cell', async function () { await withTestNotebook( [ - ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}], - ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}] ], @@ -196,9 +196,9 @@ suite('CellOperations', () => { test('Join cell with above - single cell', async function () { await withTestNotebook( [ - ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}], - ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}] ], @@ -445,4 +445,3 @@ suite('CellOperations', () => { }); }); }); - diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts index 14dae1280f..2f516d00ee 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/test/notebookClipboard.test.ts b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/test/notebookClipboard.test.ts index c881379f4f..0fe6bced9b 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/test/notebookClipboard.test.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/test/notebookClipboard.test.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; @@ -38,9 +38,9 @@ suite('Notebook Clipboard', () => { test('Cut multiple selected cells', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 2', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 2', 'markdown', CellKind.Markup, [], {}], ], async (editor, accessor) => { accessor.stub(INotebookService, new class extends mock() { override setToCopy() { } }); @@ -59,12 +59,12 @@ suite('Notebook Clipboard', () => { test('Cut should take folding info into account', async function () { await withTestNotebook( [ - ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}], - ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], - ['var c = 3', 'javascript', CellKind.Markdown, [], {}], - ['# header d', 'markdown', CellKind.Markdown, [], {}], + ['var c = 3', 'javascript', CellKind.Markup, [], {}], + ['# header d', 'markdown', CellKind.Markup, [], {}], ['var e = 4;', 'javascript', CellKind.Code, [], {}], ], async (editor, accessor) => { @@ -91,12 +91,12 @@ suite('Notebook Clipboard', () => { test('Copy should take folding info into account', async function () { await withTestNotebook( [ - ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}], - ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], - ['var c = 3', 'javascript', CellKind.Markdown, [], {}], - ['# header d', 'markdown', CellKind.Markdown, [], {}], + ['var c = 3', 'javascript', CellKind.Markup, [], {}], + ['# header d', 'markdown', CellKind.Markup, [], {}], ['var e = 4;', 'javascript', CellKind.Code, [], {}], ], async (editor, accessor) => { @@ -129,9 +129,9 @@ suite('Notebook Clipboard', () => { test('#119773, cut last item should not focus on the top first cell', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 2', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 2', 'markdown', CellKind.Markup, [], {}], ], async (editor, accessor) => { accessor.stub(INotebookService, new class extends mock() { override setToCopy() { } }); @@ -148,9 +148,9 @@ suite('Notebook Clipboard', () => { test('#119771, undo paste should restore selections', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 2', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 2', 'markdown', CellKind.Markup, [], {}], ], async (editor, accessor) => { accessor.stub(INotebookService, new class extends mock() { @@ -183,9 +183,9 @@ suite('Notebook Clipboard', () => { test('copy cell from ui still works if the target cell is not part of a selection', async () => { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 2', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 2', 'markdown', CellKind.Markup, [], {}], ], async (editor, accessor) => { let _toCopy: NotebookCellTextModel[] = []; @@ -213,10 +213,10 @@ suite('Notebook Clipboard', () => { test('cut cell from ui still works if the target cell is not part of a selection', async () => { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 2', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 3', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 2', 'markdown', CellKind.Markup, [], {}], + ['paragraph 3', 'markdown', CellKind.Markup, [], {}], ], async (editor, accessor) => { accessor.stub(INotebookService, new class extends mock() { @@ -255,10 +255,10 @@ suite('Notebook Clipboard', () => { test('cut focus cell still works if the focus is not part of any selection', async () => { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 2', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 3', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 2', 'markdown', CellKind.Markup, [], {}], + ['paragraph 3', 'markdown', CellKind.Markup, [], {}], ], async (editor, accessor) => { accessor.stub(INotebookService, new class extends mock() { @@ -280,10 +280,10 @@ suite('Notebook Clipboard', () => { test('cut focus cell still works if the focus is not part of any selection 2', async () => { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 2', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 3', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 2', 'markdown', CellKind.Markup, [], {}], + ['paragraph 3', 'markdown', CellKind.Markup, [], {}], ], async (editor, accessor) => { accessor.stub(INotebookService, new class extends mock() { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index aba9e13213..8ddf67e950 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -18,21 +18,27 @@ import { InputFocusedContext, InputFocusedContextKey } from 'vs/platform/context import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; -import { BaseCellRenderTemplate, CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID, EXPAND_CELL_INPUT_COMMAND_ID, getNotebookEditorFromEditorPane, IActiveNotebookEditor, ICellViewModel, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_HAS_RUNNING_CELL, CHANGE_CELL_LANGUAGE, QUIT_EDIT_CELL_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { BaseCellRenderTemplate, CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID, EXPAND_CELL_INPUT_COMMAND_ID, getNotebookEditorFromEditorPane, IActiveNotebookEditor, ICellViewModel, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_HAS_RUNNING_CELL, CHANGE_CELL_LANGUAGE, QUIT_EDIT_CELL_COMMAND_ID, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON, NOTEBOOK_HAS_OUTPUTS } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellEditType, CellKind, ICellEditOperation, isDocumentExcludePattern, NotebookCellMetadata, NotebookCellExecutionState, TransientCellMetadata, TransientDocumentMetadata, SelectionStateType, ICellReplaceEdit } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; +import { ICellRange, isICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; 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 { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; -import { EditorsOrder } from 'vs/workbench/common/editor'; +import { EditorsOrder, IEditorCommandsContext } from 'vs/workbench/common/editor'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { Iterable } from 'vs/base/common/iterator'; +import { flatten, maxIndex, minIndex } from 'vs/base/common/arrays'; +import { Codicon } from 'vs/base/common/codicons'; + +// Kernel Command +export const SELECT_KERNEL_ID = 'notebook.selectKernel'; // Notebook Commands const EXECUTE_NOTEBOOK_COMMAND_ID = 'notebook.execute'; @@ -56,6 +62,8 @@ const DELETE_CELL_COMMAND_ID = 'notebook.cell.delete'; const CANCEL_CELL_COMMAND_ID = 'notebook.cell.cancelExecution'; const EXECUTE_CELL_SELECT_BELOW = 'notebook.cell.executeAndSelectBelow'; const EXECUTE_CELL_INSERT_BELOW = 'notebook.cell.executeAndInsertBelow'; +const EXECUTE_CELL_AND_BELOW = 'notebook.cell.executeCellAndBelow'; +const EXECUTE_CELLS_ABOVE = 'notebook.cell.executeCellsAbove'; const CLEAR_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.clearOutputs'; const CENTER_ACTIVE_CELL = 'notebook.centerActiveCell'; @@ -89,7 +97,7 @@ export interface INotebookActionContext { readonly cell?: ICellViewModel; readonly notebookEditor: IActiveNotebookEditor; readonly ui?: boolean; - readonly selectedCells?: ICellViewModel[]; + readonly selectedCells?: readonly ICellViewModel[]; } export interface INotebookCellActionContext extends INotebookActionContext { @@ -175,11 +183,11 @@ export abstract class NotebookAction extends Action2 { super(desc); } - async run(accessor: ServicesAccessor, context?: any): Promise { + async run(accessor: ServicesAccessor, context?: any, ...additionalArgs: any[]): Promise { const isFromUI = !!context; const from = isFromUI ? (this.isNotebookActionContext(context) ? 'notebookToolbar' : 'editorToolbar') : undefined; if (!this.isNotebookActionContext(context)) { - context = this.getEditorContextFromArgsOrActive(accessor, context); + context = this.getEditorContextFromArgsOrActive(accessor, context, ...additionalArgs); if (!context) { return; } @@ -190,7 +198,7 @@ export abstract class NotebookAction extends Action2 { telemetryService.publicLog2('workbenchActionExecuted', { id: this.desc.id, from: from }); } - this.runWithContext(accessor, context); + return this.runWithContext(accessor, context); } abstract runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise; @@ -199,11 +207,78 @@ export abstract class NotebookAction extends Action2 { return !!context && !!(context as INotebookActionContext).notebookEditor; } - protected getEditorContextFromArgsOrActive(accessor: ServicesAccessor, context?: any): INotebookActionContext | undefined { + protected getEditorContextFromArgsOrActive(accessor: ServicesAccessor, context?: any, ...additionalArgs: any[]): INotebookActionContext | undefined { return getContextFromActiveEditor(accessor.get(IEditorService)); } } +// todo@rebornix, replace NotebookAction with this +export abstract class NotebookMultiCellAction extends Action2 { + constructor(desc: IAction2Options) { + if (desc.f1 !== false) { + desc.f1 = false; + const f1Menu = { + id: MenuId.CommandPalette, + when: NOTEBOOK_IS_ACTIVE_EDITOR + }; + + if (!desc.menu) { + desc.menu = []; + } else if (!Array.isArray(desc.menu)) { + desc.menu = [desc.menu]; + } + + desc.menu = [ + ...desc.menu, + f1Menu + ]; + } + + desc.category = NOTEBOOK_ACTIONS_CATEGORY; + + super(desc); + } + + abstract parseArgs(accessor: ServicesAccessor, ...args: any[]): T | undefined; + abstract runWithContext(accessor: ServicesAccessor, context: T): Promise; + + protected isNotebookActionContext(context?: unknown): context is INotebookActionContext { + return !!context && !!(context as INotebookActionContext).notebookEditor; + } + private isEditorContext(context?: unknown): boolean { + return !!context && (context as IEditorCommandsContext).groupId !== undefined; + } + protected getEditorFromArgsOrActivePane(accessor: ServicesAccessor, context?: UriComponents): IActiveNotebookEditor | undefined { + const editorFromUri = getContextFromUri(accessor, context)?.notebookEditor; + + if (editorFromUri) { + return editorFromUri; + } + + const editor = getNotebookEditorFromEditorPane(accessor.get(IEditorService).activeEditorPane); + if (!editor || !editor.hasModel()) { + return undefined; // {{SQL CARBON EDIT}} Strict nulls + } + + return editor; + } + + async run(accessor: ServicesAccessor, ...additionalArgs: any[]): Promise { + const context = additionalArgs[0]; + const isFromCellToolbar = this.isNotebookActionContext(context); + const isFromEditorToolbar = this.isEditorContext(context); + const from = isFromCellToolbar ? 'cellToolbar' : (isFromEditorToolbar ? 'editorToolbar' : 'other'); + const parsedArgs = this.parseArgs(accessor, ...additionalArgs); + if (!parsedArgs) { + return; + } + + const telemetryService = accessor.get(ITelemetryService); + telemetryService.publicLog2('workbenchActionExecuted', { id: this.desc.id, from: from }); + return this.runWithContext(accessor, parsedArgs); + } +} + export abstract class NotebookCellAction extends NotebookAction { protected isCellActionContext(context?: unknown): context is INotebookCellActionContext { return !!context && !!(context as INotebookCellActionContext).notebookEditor && !!(context as INotebookCellActionContext).cell; @@ -236,19 +311,195 @@ export abstract class NotebookCellAction extends abstract override runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise; } -const executeCellCondition = ContextKeyExpr.or( - ContextKeyExpr.and( - ContextKeyExpr.or( - ContextKeyExpr.equals(NOTEBOOK_CELL_EXECUTION_STATE.key, 'idle'), - ContextKeyExpr.equals(NOTEBOOK_CELL_EXECUTION_STATE.key, 'succeeded'), - ContextKeyExpr.equals(NOTEBOOK_CELL_EXECUTION_STATE.key, 'failed'), - ), - ContextKeyExpr.greater(NOTEBOOK_KERNEL_COUNT.key, 0)), - NOTEBOOK_CELL_TYPE.isEqualTo('markdown')); +const executeCellCondition = ContextKeyExpr.and( + NOTEBOOK_CELL_TYPE.isEqualTo('code'), + ContextKeyExpr.or( + ContextKeyExpr.equals(NOTEBOOK_CELL_EXECUTION_STATE.key, 'idle'), + ContextKeyExpr.equals(NOTEBOOK_CELL_EXECUTION_STATE.key, 'succeeded'), + ContextKeyExpr.equals(NOTEBOOK_CELL_EXECUTION_STATE.key, 'failed'), + ), + ContextKeyExpr.greater(NOTEBOOK_KERNEL_COUNT.key, 0)); const executeNotebookCondition = ContextKeyExpr.greater(NOTEBOOK_KERNEL_COUNT.key, 0); -registerAction2(class ExecuteCell extends NotebookCellAction { +interface IMultiCellArgs { + ranges: ICellRange[]; + document?: URI; +} + +function isMultiCellArgs(arg: unknown): arg is IMultiCellArgs { + if (arg === undefined) { + return false; + } + const ranges = (arg as IMultiCellArgs).ranges; + if (!ranges) { + return false; + } + + if (!Array.isArray(ranges) || ranges.some(range => !isICellRange(range))) { + return false; + } + + if ((arg as IMultiCellArgs).document) { + const uri = URI.revive((arg as IMultiCellArgs).document); + + if (!uri) { + return false; + } + } + + return true; +} + +function isNotebookActionContext(context?: unknown): context is INotebookActionContext { + return !!context && !!(context as INotebookActionContext).notebookEditor; +} + +function getEditorFromArgsOrActivePane(accessor: ServicesAccessor, context?: UriComponents): IActiveNotebookEditor | undefined { + const editorFromUri = getContextFromUri(accessor, context)?.notebookEditor; + + if (editorFromUri) { + return editorFromUri; + } + + const editor = getNotebookEditorFromEditorPane(accessor.get(IEditorService).activeEditorPane); + if (!editor || !editor.hasModel()) { + return undefined; // {{SQL CARBON EDIT}} Strict nulls + } + + return editor; +} + +function parseMultiCellExecutionArgs(accessor: ServicesAccessor, ...args: any[]) { + const firstArg = args[0]; + if (isNotebookActionContext(firstArg)) { + // from UI + return firstArg; + } + + // then it's from keybindings or commands + // todo@rebornix assertType + if (isMultiCellArgs(firstArg)) { + const editor = getEditorFromArgsOrActivePane(accessor, firstArg.document); + if (!editor) { + return undefined; // {{SQL CARBON EDIT}} Strict nulls + } + + const ranges = firstArg.ranges; + const selectedCells = flatten(ranges.map(range => editor.viewModel.getCells(range).slice(0))); + return { + notebookEditor: editor, + selectedCells + }; + } + + // handle legacy arguments + if (isICellRange(firstArg)) { + // cellRange, document + const secondArg = args[1]; + const editor = getEditorFromArgsOrActivePane(accessor, secondArg); + if (!editor) { + return undefined; // {{SQL CARBON EDIT}} Strict nulls + } + + return { + notebookEditor: editor, + selectedCells: editor.viewModel.getCells(firstArg) + }; + } + + // let's just execute the active cell + const context = getContextFromActiveEditor(accessor.get(IEditorService)); + return context; +} + +registerAction2(class ExecuteAboveCells extends NotebookMultiCellAction { + constructor() { + super({ + id: EXECUTE_CELLS_ABOVE, + precondition: executeCellCondition, + title: localize('notebookActions.executeAbove', "Execute Above Cells"), + menu: [ + { + id: MenuId.NotebookCellExecute, + when: executeCellCondition + }, + { + id: MenuId.NotebookCellTitle, + group: 'inline', + when: ContextKeyExpr.and( + executeCellCondition, + ContextKeyExpr.equals('config.notebook.consolidatedRunButton', false)) + } + ], + icon: icons.executeAboveIcon + }); + } + + parseArgs(accessor: ServicesAccessor, ...args: any[]): INotebookActionContext | undefined { + return parseMultiCellExecutionArgs(accessor, ...args); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { + let endCellIdx: number | undefined = undefined; + if (context.ui && context.cell) { + endCellIdx = context.notebookEditor.viewModel.getCellIndex(context.cell); + } else if (context.selectedCells) { + endCellIdx = maxIndex(context.selectedCells, cell => context.notebookEditor.viewModel.getCellIndex(cell)); + } + + if (typeof endCellIdx === 'number') { + const range = { start: 0, end: endCellIdx }; + const cells = context.notebookEditor.viewModel.getCells(range); + context.notebookEditor.executeNotebookCells(cells); + } + } +}); + +registerAction2(class ExecuteCellAndBelow extends NotebookMultiCellAction { + constructor() { + super({ + id: EXECUTE_CELL_AND_BELOW, + precondition: executeCellCondition, + title: localize('notebookActions.executeBelow', "Execute Cell and Below"), + menu: [ + { + id: MenuId.NotebookCellExecute, + when: executeCellCondition, + }, + { + id: MenuId.NotebookCellTitle, + group: 'inline', + when: ContextKeyExpr.and( + executeCellCondition, + ContextKeyExpr.equals('config.notebook.consolidatedRunButton', false)) + } + ], + icon: icons.executeBelowIcon + }); + } + + parseArgs(accessor: ServicesAccessor, ...args: any[]): INotebookActionContext | undefined { + return parseMultiCellExecutionArgs(accessor, ...args); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { + let startCellIdx: number | undefined = undefined; + if (context.ui && context.cell) { + startCellIdx = context.notebookEditor.viewModel.getCellIndex(context.cell); + } else if (context.selectedCells) { + startCellIdx = minIndex(context.selectedCells, cell => context.notebookEditor.viewModel.getCellIndex(cell)); + } + + if (typeof startCellIdx === 'number') { + const range = { start: startCellIdx, end: context.notebookEditor.viewModel.viewCells.length }; + const cells = context.notebookEditor.viewModel.getCells(range); + context.notebookEditor.executeNotebookCells(cells); + } + } +}); + +registerAction2(class ExecuteCell extends NotebookMultiCellAction { constructor() { super({ id: EXECUTE_CELL_COMMAND_ID, @@ -271,25 +522,35 @@ registerAction2(class ExecuteCell extends NotebookCellAction { description: localize('notebookActions.execute', "Execute Cell"), args: [ { - name: 'range', - description: 'The cell range', + name: 'options', + description: 'The cell range options', schema: { 'type': 'object', - 'required': ['start', 'end'], + 'required': ['ranges'], 'properties': { - 'start': { - 'type': 'number' + 'ranges': { + 'type': 'array', + items: [ + { + 'type': 'object', + 'required': ['start', 'end'], + 'properties': { + 'start': { + 'type': 'number' + }, + 'end': { + 'type': 'number' + } + } + } + ] }, - 'end': { - 'type': 'number' + 'document': { + 'type': 'object', + 'description': 'The document uri', } } } - }, - { - name: 'uri', - description: 'The document uri', - constraint: URI } ] }, @@ -297,47 +558,11 @@ registerAction2(class ExecuteCell extends NotebookCellAction { }); } - override getCellContextFromArgs(accessor: ServicesAccessor, context?: ICellRange, ...additionalArgs: any[]): INotebookCellActionContext | undefined { - if (!context) { - return undefined; // {{SQL CARBON EDIT}} Fix strict null - } - - if (typeof context.start !== 'number' || typeof context.end !== 'number' || context.start >= context.end) { - throw new Error(`The first argument '${context}' is not a valid CellRange`); - } - - if (additionalArgs.length && additionalArgs[0]) { - const uri = URI.revive(additionalArgs[0]); - - if (!uri) { - throw new Error(`The second argument '${uri}' is not a valid Uri`); - } - - const widget = getWidgetFromUri(accessor, uri); - if (widget) { - return { - notebookEditor: widget, - cell: widget.viewModel.cellAt(context.start)! - }; - } else { - throw new Error(`There is no editor opened for resource ${uri}`); - } - } - - const activeEditorContext = this.getEditorContextFromArgsOrActive(accessor); - - if (!activeEditorContext || !activeEditorContext.notebookEditor.viewModel || context.start >= activeEditorContext.notebookEditor.viewModel.length) { - return undefined; // {{SQL CARBON EDIT}} Fix strict null - } - - // TODO@rebornix, support multiple cells - return { - notebookEditor: activeEditorContext.notebookEditor, - cell: activeEditorContext.notebookEditor.viewModel.cellAt(context.start)! - }; + parseArgs(accessor: ServicesAccessor, ...args: any[]): INotebookActionContext | undefined { + return parseMultiCellExecutionArgs(accessor, ...args); } - async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { return runCell(accessor, context); } }); @@ -347,7 +572,7 @@ const cellCancelCondition = ContextKeyExpr.or( ContextKeyExpr.equals(NOTEBOOK_CELL_EXECUTION_STATE.key, 'pending'), ); -registerAction2(class CancelExecuteCell extends NotebookCellAction { +registerAction2(class CancelExecuteCell extends NotebookMultiCellAction { constructor() { super({ id: CANCEL_CELL_COMMAND_ID, @@ -363,65 +588,51 @@ registerAction2(class CancelExecuteCell extends NotebookCellAction { description: localize('notebookActions.cancel', "Stop Cell Execution"), args: [ { - name: 'range', - description: 'The cell range', + name: 'options', + description: 'The cell range options', schema: { 'type': 'object', - 'required': ['start', 'end'], + 'required': ['ranges'], 'properties': { - 'start': { - 'type': 'number' + 'ranges': { + 'type': 'array', + items: [ + { + 'type': 'object', + 'required': ['start', 'end'], + 'properties': { + 'start': { + 'type': 'number' + }, + 'end': { + 'type': 'number' + } + } + } + ] }, - 'end': { - 'type': 'number' + 'document': { + 'type': 'object', + 'description': 'The document uri', } } } - }, - { - name: 'uri', - description: 'The document uri', - constraint: URI } ] }, }); } - override 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}} - } - - if (additionalArgs.length && additionalArgs[0]) { - const uri = URI.revive(additionalArgs[0]); - - if (uri) { - const widget = getWidgetFromUri(accessor, uri); - if (widget) { - return { - notebookEditor: widget, - cell: widget.viewModel.cellAt(context.start)! - }; - } - } - } - - const activeEditorContext = this.getEditorContextFromArgsOrActive(accessor); - - if (!activeEditorContext || !activeEditorContext.notebookEditor.viewModel || context.start >= activeEditorContext.notebookEditor.viewModel.length) { - return undefined; // {{SQL CARBON EDIT}} Fix strict null - } - - // TODO@rebornix, support multiple cells - return { - notebookEditor: activeEditorContext.notebookEditor, - cell: activeEditorContext.notebookEditor.viewModel.cellAt(context.start)! - }; + parseArgs(accessor: ServicesAccessor, ...args: any[]): INotebookActionContext | undefined { + return parseMultiCellExecutionArgs(accessor, ...args); } - async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - return context.notebookEditor.cancelNotebookCells(Iterable.single(context.cell)); + async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { + if (context.ui && context.cell) { + return context.notebookEditor.cancelNotebookCells(Iterable.single(context.cell)); + } else if (context.selectedCells) { + return context.notebookEditor.cancelNotebookCells(context.selectedCells); + } } }); @@ -448,7 +659,7 @@ registerAction2(class ExecuteCellSelectBelow extends NotebookCellAction { constructor() { super({ id: EXECUTE_CELL_SELECT_BELOW, - precondition: executeCellCondition, + precondition: ContextKeyExpr.or(executeCellCondition, NOTEBOOK_CELL_TYPE.isEqualTo('markup')), title: localize('notebookActions.executeAndSelectBelow', "Execute Notebook Cell and Select Below"), keybinding: { when: NOTEBOOK_CELL_LIST_FOCUSED, @@ -464,20 +675,33 @@ registerAction2(class ExecuteCellSelectBelow extends NotebookCellAction { return; } - const executionP = runCell(accessor, context); - - // Try to select below, fall back on inserting - const nextCell = context.notebookEditor.viewModel.cellAt(idx + 1); - if (nextCell) { - context.notebookEditor.focusNotebookCell(nextCell, 'container'); - } else { - const newCell = context.notebookEditor.insertNotebookCell(context.cell, CellKind.Code, 'below'); - if (newCell) { - context.notebookEditor.focusNotebookCell(newCell, 'editor'); + if (context.cell.cellKind === CellKind.Markup) { + const nextCell = context.notebookEditor.viewModel.cellAt(idx + 1); + if (nextCell) { + context.notebookEditor.focusNotebookCell(nextCell, 'container'); + } else { + const newCell = context.notebookEditor.insertNotebookCell(context.cell, CellKind.Markup, 'below'); + if (newCell) { + context.notebookEditor.focusNotebookCell(newCell, 'editor'); + } } - } + return; + } else { + const executionP = runCell(accessor, context); - return executionP; + // Try to select below, fall back on inserting + const nextCell = context.notebookEditor.viewModel.cellAt(idx + 1); + if (nextCell) { + context.notebookEditor.focusNotebookCell(nextCell, 'container'); + } else { + const newCell = context.notebookEditor.insertNotebookCell(context.cell, CellKind.Code, 'below'); + if (newCell) { + context.notebookEditor.focusNotebookCell(newCell, 'editor'); + } + } + + return executionP; + } } }); @@ -508,7 +732,7 @@ registerAction2(class ExecuteCellInsertBelow extends NotebookCellAction { } }); -registerAction2(class extends NotebookAction { +registerAction2(class RenderAllMarkdownCellsAction extends NotebookAction { constructor() { super({ id: RENDER_ALL_MARKDOWN_CELLS, @@ -521,28 +745,44 @@ registerAction2(class extends NotebookAction { } }); -registerAction2(class extends NotebookAction { +registerAction2(class ExecuteNotebookAction extends NotebookAction { constructor() { super({ id: EXECUTE_NOTEBOOK_COMMAND_ID, - title: localize('notebookActions.executeNotebook', "Execute Notebook (Run all cells)"), + title: localize('notebookActions.executeNotebook', "Run All"), icon: icons.executeAllIcon, description: { - description: localize('notebookActions.executeNotebook', "Execute Notebook (Run all cells)"), + description: localize('notebookActions.executeNotebook', "Run All"), args: [ { name: 'uri', - description: 'The document uri', - constraint: URI + description: 'The document uri' } ] }, - menu: { - id: MenuId.EditorTitle, - order: -1, - group: 'navigation', - when: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, executeNotebookCondition, ContextKeyExpr.or(NOTEBOOK_INTERRUPTIBLE_KERNEL.toNegated(), NOTEBOOK_HAS_RUNNING_CELL.toNegated())), - } + menu: [ + { + id: MenuId.EditorTitle, + order: -1, + group: 'navigation', + when: ContextKeyExpr.and( + NOTEBOOK_IS_ACTIVE_EDITOR, + executeNotebookCondition, + ContextKeyExpr.or(NOTEBOOK_INTERRUPTIBLE_KERNEL.toNegated(), NOTEBOOK_HAS_RUNNING_CELL.toNegated()), + ContextKeyExpr.notEquals('config.notebook.globalToolbar', true) + ) + }, + { + id: MenuId.NotebookToolbar, + order: -1, + group: 'navigation/execute', + when: ContextKeyExpr.and( + executeNotebookCondition, + ContextKeyExpr.or(NOTEBOOK_INTERRUPTIBLE_KERNEL.toNegated(), NOTEBOOK_HAS_RUNNING_CELL.toNegated()), + ContextKeyExpr.equals('config.notebook.globalToolbar', true) + ) + } + ] }); } @@ -569,7 +809,7 @@ registerAction2(class extends NotebookAction { function renderAllMarkdownCells(context: INotebookActionContext): void { context.notebookEditor.viewModel.viewCells.forEach(cell => { - if (cell.cellKind === CellKind.Markdown) { + if (cell.cellKind === CellKind.Markup) { cell.updateEditState(CellEditState.Preview, 'renderAllMarkdownCells'); } }); @@ -579,10 +819,10 @@ registerAction2(class CancelNotebook extends NotebookAction { constructor() { super({ id: CANCEL_NOTEBOOK_COMMAND_ID, - title: localize('notebookActions.cancelNotebook', "Stop Notebook Execution"), + title: localize('notebookActions.cancelNotebook', "Stop Execution"), icon: icons.stopIcon, description: { - description: localize('notebookActions.cancelNotebook', "Stop Notebook Execution"), + description: localize('notebookActions.cancelNotebook', "Stop Execution"), args: [ { name: 'uri', @@ -591,12 +831,29 @@ registerAction2(class CancelNotebook extends NotebookAction { } ] }, - menu: { - id: MenuId.EditorTitle, - order: -1, - group: 'navigation', - when: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL) - } + menu: [ + { + id: MenuId.EditorTitle, + order: -1, + group: 'navigation', + when: ContextKeyExpr.and( + NOTEBOOK_IS_ACTIVE_EDITOR, + NOTEBOOK_HAS_RUNNING_CELL, + NOTEBOOK_INTERRUPTIBLE_KERNEL, + ContextKeyExpr.notEquals('config.notebook.globalToolbar', true) + ) + }, + { + id: MenuId.NotebookToolbar, + order: -1, + group: 'navigation/execute', + when: ContextKeyExpr.and( + NOTEBOOK_HAS_RUNNING_CELL, + NOTEBOOK_INTERRUPTIBLE_KERNEL, + ContextKeyExpr.equals('config.notebook.globalToolbar', true) + ) + } + ] }); } @@ -623,7 +880,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorContext, { when: NOTEBOOK_EDITOR_FOCUSED }); -registerAction2(class extends NotebookCellAction { +registerAction2(class ChangeCellToCodeAction extends NotebookCellAction { constructor() { super({ id: CHANGE_CELL_TO_CODE_COMMAND_ID, @@ -633,10 +890,10 @@ registerAction2(class extends NotebookCellAction { primary: KeyCode.KEY_Y, weight: KeybindingWeight.WorkbenchContrib }, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_CELL_TYPE.isEqualTo('markdown')), + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_CELL_TYPE.isEqualTo('markup')), menu: { id: MenuId.NotebookCellTitle, - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_TYPE.isEqualTo('markdown')), + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_TYPE.isEqualTo('markup')), group: CellOverflowToolbarGroups.Edit, } }); @@ -647,7 +904,7 @@ registerAction2(class extends NotebookCellAction { } }); -registerAction2(class extends NotebookCellAction { +registerAction2(class ChangeCellToMarkdownAction extends NotebookCellAction { constructor() { super({ id: CHANGE_CELL_TO_MARKDOWN_COMMAND_ID, @@ -667,14 +924,11 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - await changeCellToKind(CellKind.Markdown, context); + await changeCellToKind(CellKind.Markup, context); } }); -async function runCell(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - if (context.cell.metadata?.runState === NotebookCellExecutionState.Executing) { - return; - } +async function runCell(accessor: ServicesAccessor, context: INotebookActionContext): Promise { const editorGroupService = accessor.get(IEditorGroupsService); const group = editorGroupService.activeGroup; @@ -685,11 +939,13 @@ async function runCell(accessor: ServicesAccessor, context: INotebookCellActionC } } - if (context.cell.cellKind === CellKind.Markdown) { - context.notebookEditor.focusNotebookCell(context.cell, 'container'); - return; - } else { + if (context.ui && context.cell) { + if (context.cell.internalMetadata.runState === NotebookCellExecutionState.Executing) { + return; + } return context.notebookEditor.executeNotebookCells(Iterable.single(context.cell)); + } else if (context.selectedCells) { + return context.notebookEditor.executeNotebookCells(context.selectedCells); } } @@ -751,11 +1007,17 @@ abstract class InsertCellCommand extends NotebookAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { - context.notebookEditor.insertNotebookCell(context.cell, this.kind, this.direction, undefined, true); + if (context.cell) { + context.notebookEditor.insertNotebookCell(context.cell, this.kind, this.direction, undefined, true); + } else { + const focusRange = context.notebookEditor.getFocus(); + const next = focusRange.end - 1; + context.notebookEditor.insertNotebookCell(context.notebookEditor.viewModel.viewCells[next], this.kind, this.direction, undefined, true); + } } } -registerAction2(class extends InsertCellCommand { +registerAction2(class InsertCodeCellAboveAction extends InsertCellCommand { constructor() { super( { @@ -776,7 +1038,7 @@ registerAction2(class extends InsertCellCommand { } }); -registerAction2(class extends InsertCellCommand { +registerAction2(class InsertCodeCellBelowAction extends InsertCellCommand { constructor() { super( { @@ -797,7 +1059,7 @@ registerAction2(class extends InsertCellCommand { } }); -registerAction2(class extends NotebookAction { +registerAction2(class InsertCodeCellAtTopAction extends NotebookAction { constructor() { super( { @@ -822,7 +1084,7 @@ registerAction2(class extends NotebookAction { } }); -registerAction2(class extends NotebookAction { +registerAction2(class InsertMarkdownCellAtTopAction extends NotebookAction { constructor() { super( { @@ -840,7 +1102,7 @@ registerAction2(class extends NotebookAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { - const newCell = context.notebookEditor.insertNotebookCell(undefined, CellKind.Markdown, 'above', undefined, true); + const newCell = context.notebookEditor.insertNotebookCell(undefined, CellKind.Markup, 'above', undefined, true); if (newCell) { context.notebookEditor.focusNotebookCell(newCell, 'editor'); } @@ -855,7 +1117,41 @@ MenuRegistry.appendMenuItem(MenuId.NotebookCellBetween, { }, order: 0, group: 'inline', - when: NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true) + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + ContextKeyExpr.notEquals('config.notebook.experimental.insertToolbarAlignment', 'left') + ) +}); + +MenuRegistry.appendMenuItem(MenuId.NotebookCellBetween, { + command: { + id: INSERT_CODE_CELL_BELOW_COMMAND_ID, + title: localize('notebookActions.menu.insertCode.minimalToolbar', "Add Code"), + icon: Codicon.add, + tooltip: localize('notebookActions.menu.insertCode.tooltip', "Add Code Cell") + }, + order: 0, + group: 'inline', + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + ContextKeyExpr.equals('config.notebook.experimental.insertToolbarAlignment', 'left') + ) +}); + +MenuRegistry.appendMenuItem(MenuId.NotebookToolbar, { + command: { + id: INSERT_CODE_CELL_BELOW_COMMAND_ID, + icon: Codicon.add, + title: localize('notebookActions.menu.insertCode.ontoolbar', "Code"), + tooltip: localize('notebookActions.menu.insertCode.tooltip', "Add Code Cell") + }, + order: -5, + group: 'navigation/add', + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + ContextKeyExpr.notEquals('config.notebook.insertToolbarLocation', 'betweenCells'), + ContextKeyExpr.notEquals('config.notebook.insertToolbarLocation', 'hidden') + ) }); MenuRegistry.appendMenuItem(MenuId.NotebookCellListTop, { @@ -866,10 +1162,28 @@ MenuRegistry.appendMenuItem(MenuId.NotebookCellListTop, { }, order: 0, group: 'inline', - when: NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true) + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + ContextKeyExpr.notEquals('config.notebook.experimental.insertToolbarAlignment', 'left') + ) }); -registerAction2(class extends InsertCellCommand { +MenuRegistry.appendMenuItem(MenuId.NotebookCellListTop, { + command: { + id: INSERT_CODE_CELL_AT_TOP_COMMAND_ID, + title: localize('notebookActions.menu.insertCode.minimaltoolbar', "Add Code"), + icon: Codicon.add, + tooltip: localize('notebookActions.menu.insertCode.tooltip', "Add Code Cell") + }, + order: 0, + group: 'inline', + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + ContextKeyExpr.equals('config.notebook.experimental.insertToolbarAlignment', 'left') + ) +}); + +registerAction2(class InsertMarkdownCellAboveAction extends InsertCellCommand { constructor() { super( { @@ -880,12 +1194,12 @@ registerAction2(class extends InsertCellCommand { order: 2 } }, - CellKind.Markdown, + CellKind.Markup, 'above'); } }); -registerAction2(class extends InsertCellCommand { +registerAction2(class InsertMarkdownCellBelowAction extends InsertCellCommand { constructor() { super( { @@ -896,7 +1210,7 @@ registerAction2(class extends InsertCellCommand { order: 3 } }, - CellKind.Markdown, + CellKind.Markup, 'below'); } }); @@ -909,7 +1223,26 @@ MenuRegistry.appendMenuItem(MenuId.NotebookCellBetween, { }, order: 1, group: 'inline', - when: NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true) + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + ContextKeyExpr.notEquals('config.notebook.experimental.insertToolbarAlignment', 'left') + ) +}); + +MenuRegistry.appendMenuItem(MenuId.NotebookToolbar, { + command: { + id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, + icon: Codicon.add, + title: localize('notebookActions.menu.insertMarkdown.ontoolbar', "Markdown"), + tooltip: localize('notebookActions.menu.insertMarkdown.tooltip', "Add Markdown Cell") + }, + order: -5, + group: 'navigation/add', + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + ContextKeyExpr.notEquals('config.notebook.insertToolbarLocation', 'betweenCells'), + ContextKeyExpr.notEquals('config.notebook.insertToolbarLocation', 'hidden') + ) }); MenuRegistry.appendMenuItem(MenuId.NotebookCellListTop, { @@ -920,10 +1253,13 @@ MenuRegistry.appendMenuItem(MenuId.NotebookCellListTop, { }, order: 1, group: 'inline', - when: NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true) + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + ContextKeyExpr.notEquals('config.notebook.experimental.insertToolbarAlignment', 'left') + ) }); -registerAction2(class extends NotebookCellAction { +registerAction2(class EditCellAction extends NotebookCellAction { constructor() { super( { @@ -937,7 +1273,7 @@ registerAction2(class extends NotebookCellAction { menu: { id: MenuId.NotebookCellTitle, when: ContextKeyExpr.and( - NOTEBOOK_CELL_TYPE.isEqualTo('markdown'), + NOTEBOOK_CELL_TYPE.isEqualTo('markup'), NOTEBOOK_CELL_MARKDOWN_EDIT_MODE.toNegated(), NOTEBOOK_CELL_EDITABLE), order: CellToolbarOrder.EditCell, @@ -952,7 +1288,14 @@ registerAction2(class extends NotebookCellAction { } }); -registerAction2(class extends NotebookCellAction { +const quitEditCondition = ContextKeyExpr.and( + NOTEBOOK_EDITOR_FOCUSED, + InputFocusedContext, + EditorContextKeys.hoverVisible.toNegated(), + EditorContextKeys.hasNonEmptySelection.toNegated(), + EditorContextKeys.hasMultipleSelections.toNegated() +); +registerAction2(class QuitEditCellAction extends NotebookCellAction { constructor() { super( { @@ -961,29 +1304,35 @@ registerAction2(class extends NotebookCellAction { menu: { id: MenuId.NotebookCellTitle, when: ContextKeyExpr.and( - NOTEBOOK_CELL_TYPE.isEqualTo('markdown'), + NOTEBOOK_CELL_TYPE.isEqualTo('markup'), NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_EDITABLE), order: CellToolbarOrder.SaveCell, group: CELL_TITLE_CELL_GROUP_ID }, icon: icons.stopEditIcon, - keybinding: { - when: ContextKeyExpr.and( - NOTEBOOK_EDITOR_FOCUSED, - InputFocusedContext, - EditorContextKeys.hoverVisible.toNegated(), - EditorContextKeys.hasNonEmptySelection.toNegated(), - EditorContextKeys.hasMultipleSelections.toNegated() - ), - primary: KeyCode.Escape, - weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT - 5 - }, + keybinding: [ + { + when: quitEditCondition, + primary: KeyCode.Escape, + weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT - 5 + }, + { + when: ContextKeyExpr.and( + quitEditCondition, + NOTEBOOK_CELL_TYPE.isEqualTo('markup')), + primary: KeyMod.WinCtrl | KeyCode.Enter, + win: { + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Enter + }, + weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT + }, + ] }); } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - if (context.cell.cellKind === CellKind.Markdown) { + if (context.cell.cellKind === CellKind.Markup) { context.cell.updateEditState(CellEditState.Preview, QUIT_EDIT_CELL_COMMAND_ID); } @@ -1054,7 +1403,7 @@ export function runDeleteAction(viewModel: NotebookViewModel, cell: ICellViewMod } } -registerAction2(class extends NotebookCellAction { +registerAction2(class DeleteCellAction extends NotebookCellAction { constructor() { super( { @@ -1086,17 +1435,23 @@ registerAction2(class extends NotebookCellAction { } }); -registerAction2(class extends NotebookCellAction { +registerAction2(class ClearCellOutputsAction extends NotebookCellAction { constructor() { super({ id: CLEAR_CELL_OUTPUTS_COMMAND_ID, title: localize('clearCellOutputs', 'Clear Cell Outputs'), - menu: { - id: MenuId.NotebookCellTitle, - when: ContextKeyExpr.and(NOTEBOOK_CELL_TYPE.isEqualTo('code'), executeNotebookCondition, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE), - order: CellToolbarOrder.ClearCellOutput, - group: CELL_TITLE_OUTPUT_GROUP_ID - }, + menu: [ + { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.and(NOTEBOOK_CELL_TYPE.isEqualTo('code'), executeNotebookCondition, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON.toNegated()), + order: CellToolbarOrder.ClearCellOutput, + group: CELL_TITLE_OUTPUT_GROUP_ID + }, + { + id: MenuId.NotebookOutputToolbar, + when: ContextKeyExpr.and(NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE) + }, + ], keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey), NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE), primary: KeyMod.Alt | KeyCode.Delete, @@ -1121,16 +1476,15 @@ registerAction2(class extends NotebookCellAction { editor.viewModel.notebookDocument.applyEdits([{ editType: CellEditType.Output, index, outputs: [] }], true, undefined, () => undefined, undefined); - if (context.cell.metadata && context.cell.metadata?.runState !== NotebookCellExecutionState.Executing) { + if (context.cell.internalMetadata.runState !== NotebookCellExecutionState.Executing) { context.notebookEditor.viewModel.notebookDocument.applyEdits([{ - editType: CellEditType.Metadata, index, metadata: { - ...context.cell.metadata, - runState: NotebookCellExecutionState.Idle, - runStartTime: undefined, - runStartTimeAdjustment: undefined, - runEndTime: undefined, - executionOrder: undefined, - lastRunSuccess: undefined + editType: CellEditType.PartialInternalMetadata, index, internalMetadata: { + runState: null, + runStartTime: null, + runStartTimeAdjustment: null, + runEndTime: null, + executionOrder: null, + lastRunSuccess: null } }], true, undefined, () => undefined, undefined); } @@ -1187,14 +1541,14 @@ registerAction2(class ChangeCellLanguageAction extends NotebookCellAction= context.end) { - return undefined; + return undefined; // {{SQL CARBON EDIT}} Strict nulls } const language = additionalArgs.length && typeof additionalArgs[0] === 'string' ? additionalArgs[0] : undefined; const activeEditorContext = this.getEditorContextFromArgsOrActive(accessor); if (!activeEditorContext || !activeEditorContext.notebookEditor.viewModel || context.start >= activeEditorContext.notebookEditor.viewModel.length) { - return undefined; // {{SQL CARBON EDIT}} Fix strict null + return undefined; // {{SQL CARBON EDIT}} Strict nulls } // TODO@rebornix, support multiple cells @@ -1229,7 +1583,7 @@ registerAction2(class ChangeCellLanguageAction extends NotebookCellAction { let description: string; - if (context.cell.cellKind === CellKind.Markdown ? (languageId === 'markdown') : (languageId === context.cell.language)) { + if (context.cell.cellKind === CellKind.Markup ? (languageId === 'markdown') : (languageId === context.cell.language)) { description = localize('languageDescription', "({0}) - Current Language", languageId); } else { description = localize('languageDescriptionConfigured', "({0})", languageId); @@ -1273,11 +1627,11 @@ registerAction2(class ChangeCellLanguageAction extends NotebookCellAction undefined, undefined); const clearExecutionMetadataEdits = editor.viewModel.notebookDocument.cells.map((cell, index) => { - if (cell.metadata && cell.metadata?.runState !== NotebookCellExecutionState.Executing) { + if (cell.internalMetadata.runState !== NotebookCellExecutionState.Executing) { return { - editType: CellEditType.Metadata, index, metadata: { - ...cell.metadata, - runState: NotebookCellExecutionState.Idle, - runStartTime: undefined, - runStartTimeAdjustment: undefined, - runEndTime: undefined, - executionOrder: undefined + editType: CellEditType.PartialInternalMetadata, index, internalMetadata: { + runState: null, + runStartTime: null, + runStartTimeAdjustment: null, + runEndTime: null, + executionOrder: null, + lastRunSuccess: null } }; } else { @@ -1356,7 +1725,7 @@ registerAction2(class extends NotebookAction { } }); -registerAction2(class extends NotebookCellAction { +registerAction2(class CenterActiveCellAction extends NotebookCellAction { constructor() { super({ id: CENTER_ACTIVE_CELL, @@ -1400,7 +1769,7 @@ abstract class ChangeNotebookCellMetadataAction extends NotebookCellAction { abstract getMetadataDelta(): NotebookCellMetadata; } -registerAction2(class extends ChangeNotebookCellMetadataAction { +registerAction2(class CollapseCellInputAction extends ChangeNotebookCellMetadataAction { constructor() { super({ id: COLLAPSE_CELL_INPUT_COMMAND_ID, @@ -1423,7 +1792,7 @@ registerAction2(class extends ChangeNotebookCellMetadataAction { } }); -registerAction2(class extends ChangeNotebookCellMetadataAction { +registerAction2(class ExpandCellInputAction extends ChangeNotebookCellMetadataAction { constructor() { super({ id: EXPAND_CELL_INPUT_COMMAND_ID, @@ -1446,7 +1815,7 @@ registerAction2(class extends ChangeNotebookCellMetadataAction { } }); -registerAction2(class extends ChangeNotebookCellMetadataAction { +registerAction2(class CollapseCellOutputAction extends ChangeNotebookCellMetadataAction { constructor() { super({ id: COLLAPSE_CELL_OUTPUT_COMMAND_ID, @@ -1469,7 +1838,7 @@ registerAction2(class extends ChangeNotebookCellMetadataAction { } }); -registerAction2(class extends ChangeNotebookCellMetadataAction { +registerAction2(class ExpandCellOuputAction extends ChangeNotebookCellMetadataAction { constructor() { super({ id: EXPAND_CELL_OUTPUT_COMMAND_ID, @@ -1492,16 +1861,69 @@ registerAction2(class extends ChangeNotebookCellMetadataAction { } }); -// Revisit once we have a story for trusted workspace -CommandsRegistry.registerCommand('notebook.trust', (accessor, args) => { - const uri = URI.revive(args as UriComponents); - const notebookService = accessor.get(INotebookService); +registerAction2(class NotebookConfigureLayoutAction extends Action2 { + constructor() { + super({ + id: 'workbench.notebook.layout.select', + title: localize('workbench.notebook.layout.select.label', "Select between Notebook Layouts"), + f1: true, + category: NOTEBOOK_ACTIONS_CATEGORY, + menu: [ + { + id: MenuId.EditorTitle, + group: 'notebookLayout', + when: ContextKeyExpr.and( + NOTEBOOK_IS_ACTIVE_EDITOR, + ContextKeyExpr.notEquals('config.notebook.globalToolbar', true), + ContextKeyExpr.equals('config.notebook.experimental.openGettingStarted', true) + ), + order: 0 + }, + { + id: MenuId.NotebookToolbar, + group: 'notebookLayout', + when: ContextKeyExpr.and( + ContextKeyExpr.equals('config.notebook.globalToolbar', true), + ContextKeyExpr.equals('config.notebook.experimental.openGettingStarted', true) + ), + order: 0 + } + ] + }); + } + run(accessor: ServicesAccessor): void { + accessor.get(ICommandService).executeCommand('workbench.action.openWalkthrough', { category: 'notebooks', step: 'notebookProfile' }, true); + } +}); - - const document = notebookService.listNotebookDocuments().find(document => document.uri.toString() === uri.toString()); - - if (document) { - document.applyEdits([{ editType: CellEditType.DocumentMetadata, metadata: { ...document.metadata, ...{ trusted: true } } }], true, undefined, () => undefined, undefined, false); +registerAction2(class NotebookConfigureLayoutAction extends Action2 { + constructor() { + super({ + id: 'workbench.notebook.layout.configure', + title: localize('workbench.notebook.layout.configure.label', "Customize Notebook Layout"), + f1: true, + category: NOTEBOOK_ACTIONS_CATEGORY, + menu: [ + { + id: MenuId.EditorTitle, + group: 'notebookLayout', + when: ContextKeyExpr.and( + NOTEBOOK_IS_ACTIVE_EDITOR, + ContextKeyExpr.notEquals('config.notebook.globalToolbar', true) + ), + order: 1 + }, + { + id: MenuId.NotebookToolbar, + group: 'notebookLayout', + when: ContextKeyExpr.equals('config.notebook.globalToolbar', true), + order: 1 + } + ] + }); + } + run(accessor: ServicesAccessor): void { + accessor.get(IPreferencesService).openSettings(false, '@tag:notebookLayout'); } }); @@ -1512,7 +1934,7 @@ CommandsRegistry.registerCommand('_resolveNotebookContentProvider', (accessor, a filenamePattern: (string | glob.IRelativePattern | { include: string | glob.IRelativePattern, exclude: string | glob.IRelativePattern; })[]; }[] => { const notebookService = accessor.get(INotebookService); - const contentProviders = notebookService.getContributedNotebookProviders(); + const contentProviders = notebookService.getContributedNotebookTypes(); return contentProviders.map(provider => { const filenamePatterns = provider.selectors.map(selector => { if (typeof selector === 'string') { 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 70908fa277..c64516161c 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts @@ -8,14 +8,9 @@ import { alert as alertFn } from 'vs/base/browser/ui/aria/aria'; import * as strings from 'vs/base/common/strings'; 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, getNotebookEditorFromEditorPane, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, INotebookEditor, CellEditState, INotebookEditorContribution, NOTEBOOK_EDITOR_FOCUSED, getNotebookEditorFromEditorPane, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { Range } from 'vs/editor/common/core/range'; import { MATCHES_LIMIT } from 'vs/editor/contrib/find/findModel'; -import { FindDecorations } from 'vs/editor/contrib/find/findDecorations'; -import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { IModelDeltaDecoration } from 'vs/editor/common/model'; -import { ICellModelDeltaDecorations, ICellModelDecorations } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { SimpleFindReplaceWidget } from 'vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget'; @@ -28,11 +23,12 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; -import { INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { StartFindAction, StartFindReplaceAction } from 'vs/editor/contrib/find/findController'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { NLS_MATCHES_LOCATION, NLS_NO_RESULTS } from 'vs/editor/contrib/find/findWidget'; +import { FindModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/findModel'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; const FIND_HIDE_TRANSITION = 'find-hide-transition'; const FIND_SHOW_TRANSITION = 'find-show-transition'; @@ -42,14 +38,10 @@ let MAX_MATCHES_COUNT_WIDTH = 69; export class NotebookFindWidget extends SimpleFindReplaceWidget implements INotebookEditorContribution { static id: string = 'workbench.notebook.find'; protected _findWidgetFocused: IContextKey; - private _findMatches: CellFindMatch[] = []; - protected _findMatchesStarts: PrefixSumComputer | null = null; - private _currentMatch: number = -1; - private _allMatchesDecorations: ICellModelDecorations[] = []; - private _currentMatchDecorations: ICellModelDecorations[] = []; private _showTimeout: number | null = null; private _hideTimeout: number | null = null; private _previousFocusElement?: HTMLElement; + private _findModel: FindModel; constructor( private readonly _notebookEditor: INotebookEditor, @@ -60,6 +52,8 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote ) { super(contextViewService, contextKeyService, themeService, new FindReplaceState(), true); + this._findModel = new FindModel(this._notebookEditor, this._state, this._configurationService); + DOM.append(this._notebookEditor.getDomNode(), this.getDomNode()); this._findWidgetFocused = KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED.bindTo(contextKeyService); @@ -80,90 +74,43 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote private _onFindInputKeyDown(e: IKeyboardEvent): void { if (e.equals(KeyCode.Enter)) { - if (this._findMatches.length) { - this.find(false); - } else { - this.set(null, true); - } + this._findModel.find(false); e.preventDefault(); return; } else if (e.equals(KeyMod.Shift | KeyCode.Enter)) { - if (this._findMatches.length) { - this.find(true); - } else { - this.set(null, true); - } + this.find(true); e.preventDefault(); return; } } protected onInputChanged(): boolean { - const val = this.inputValue; - const wordSeparators = this._configurationService.inspect('editor.wordSeparators').value; - const options: INotebookSearchOptions = { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue(), wordSeparators: wordSeparators }; - if (val) { - this._findMatches = this._notebookEditor.viewModel!.find(val, options).filter(match => match.matches.length > 0); - this.set(this._findMatches, false); - if (this._findMatches.length) { - return true; - } else { - return false; - } - } else { - this.set([], false); + this._state.change({ searchString: this.inputValue }, false); + // this._findModel.research(); + const findMatches = this._findModel.findMatches; + if (findMatches && findMatches.length) { + return true; } return false; } protected find(previous: boolean): void { - if (!this._findMatches.length) { - return; - } - - // let currCell; - if (!this._findMatchesStarts) { - this.set(this._findMatches, true); - } else { - // const currIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); - // currCell = this._findMatches[currIndex.index].cell; - - const totalVal = this._findMatchesStarts.getTotalValue(); - const nextVal = (this._currentMatch + (previous ? -1 : 1) + totalVal) % totalVal; - this._currentMatch = nextVal; - } - - const nextIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); - // const newFocusedCell = this._findMatches[nextIndex.index].cell; - this.setCurrentFindMatchDecoration(nextIndex.index, nextIndex.remainder); - this.revealCellRange(nextIndex.index, nextIndex.remainder); - - this._state.changeMatchInfo( - this._currentMatch, - this._findMatches.reduce((p, c) => p + c.matches.length, 0), - undefined - ); - - // if (currCell && currCell !== newFocusedCell && currCell.getEditState() === CellEditState.Editing && currCell.editStateSource === 'find') { - // currCell.updateEditState(CellEditState.Preview, 'find'); - // } - // this._updateMatchesCount(); + this._findModel.find(previous); } protected replaceOne() { - if (!this._findMatches.length) { + if (!this._findModel.findMatches.length) { return; } - if (!this._findMatchesStarts) { - this.set(this._findMatches, true); + this._findModel.ensureFindMatches(); + + if (this._findModel.currentMatch < 0) { + this._findModel.find(false); } - const nextIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); - const cell = this._findMatches[nextIndex.index].cell; - const match = this._findMatches[nextIndex.index].matches[nextIndex.remainder]; - + const { cell, match } = this._findModel.getCurrentMatch(); this._progressBar.infinite().show(); this._notebookEditor.viewModel!.replaceOne(cell, match.range, this.replaceValue).then(() => { @@ -174,18 +121,11 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote protected replaceAll() { this._progressBar.infinite().show(); - this._notebookEditor.viewModel!.replaceAll(this._findMatches, this.replaceValue).then(() => { + this._notebookEditor.viewModel!.replaceAll(this._findModel.findMatches, this.replaceValue).then(() => { this._progressBar.stop(); }); } - private revealCellRange(cellIndex: number, matchIndex: number) { - this._findMatches[cellIndex].cell.updateEditState(CellEditState.Editing, 'find'); - this._notebookEditor.focusElement(this._findMatches[cellIndex].cell); - this._notebookEditor.setCellEditorSelection(this._findMatches[cellIndex].cell, this._findMatches[cellIndex].matches[matchIndex].range); - this._notebookEditor.revealRangeInCenterIfOutsideViewportAsync(this._findMatches[cellIndex].cell, this._findMatches[cellIndex].matches[matchIndex].range); - } - protected findFirst(): void { } protected onFocusTrackerFocus() { @@ -207,99 +147,9 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote protected onFindInputFocusTrackerFocus(): void { } protected onFindInputFocusTrackerBlur(): void { } - private constructFindMatchesStarts() { - if (this._findMatches && this._findMatches.length) { - const values = new Uint32Array(this._findMatches.length); - for (let i = 0; i < this._findMatches.length; i++) { - values[i] = this._findMatches[i].matches.length; - } - - this._findMatchesStarts = new PrefixSumComputer(values); - } else { - this._findMatchesStarts = null; - } - } - - private set(cellFindMatches: CellFindMatch[] | null, autoStart: boolean): void { - if (!cellFindMatches || !cellFindMatches.length) { - this._findMatches = []; - this.setAllFindMatchesDecorations([]); - - this.constructFindMatchesStarts(); - this._currentMatch = -1; - this.clearCurrentFindMatchDecoration(); - return; - } - - // all matches - this._findMatches = cellFindMatches; - this.setAllFindMatchesDecorations(cellFindMatches || []); - - // current match - this.constructFindMatchesStarts(); - - if (autoStart) { - this._currentMatch = 0; - this.setCurrentFindMatchDecoration(0, 0); - } - - this._state.changeMatchInfo( - this._currentMatch, - this._findMatches.reduce((p, c) => p + c.matches.length, 0), - undefined - ); - } - - private setCurrentFindMatchDecoration(cellIndex: number, matchIndex: number) { - this._notebookEditor.changeModelDecorations(accessor => { - const findMatchesOptions: ModelDecorationOptions = FindDecorations._CURRENT_FIND_MATCH_DECORATION; - - const cell = this._findMatches[cellIndex].cell; - const match = this._findMatches[cellIndex].matches[matchIndex]; - const decorations: IModelDeltaDecoration[] = [ - { range: match.range, options: findMatchesOptions } - ]; - const deltaDecoration: ICellModelDeltaDecorations = { - ownerId: cell.handle, - decorations: decorations - }; - - this._currentMatchDecorations = accessor.deltaDecorations(this._currentMatchDecorations, [deltaDecoration]); - }); - } - - private clearCurrentFindMatchDecoration() { - this._notebookEditor.changeModelDecorations(accessor => { - this._currentMatchDecorations = accessor.deltaDecorations(this._currentMatchDecorations, []); - }); - } - - private setAllFindMatchesDecorations(cellFindMatches: CellFindMatch[]) { - this._notebookEditor.changeModelDecorations((accessor) => { - - const findMatchesOptions: ModelDecorationOptions = FindDecorations._FIND_MATCH_DECORATION; - - const deltaDecorations: ICellModelDeltaDecorations[] = cellFindMatches.map(cellFindMatch => { - const findMatches = cellFindMatch.matches; - - // Find matches - const newFindMatchesDecorations: IModelDeltaDecoration[] = new Array(findMatches.length); - for (let i = 0, len = findMatches.length; i < len; i++) { - newFindMatchesDecorations[i] = { - range: findMatches[i].range, - options: findMatchesOptions - }; - } - - return { ownerId: cellFindMatch.cell.handle, decorations: newFindMatchesDecorations }; - }); - - this._allMatchesDecorations = accessor.deltaDecorations(this._allMatchesDecorations, deltaDecorations); - }); - } - override show(initialInput?: string): void { super.show(initialInput); + this._state.change({ searchString: initialInput ?? '', isRevealed: true }, false); this._findInput.select(); if (this._showTimeout === null) { @@ -342,7 +192,8 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote override hide() { super.hide(); - this.set([], false); + this._state.change({ isRevealed: false }, false); + this._findModel.clear(); if (this._hideTimeout === null) { if (this._showTimeout !== null) { @@ -371,7 +222,7 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote } override _updateMatchesCount(): void { - if (!this._findMatches) { + if (!this._findModel || !this._findModel.findMatches) { return; } @@ -390,7 +241,7 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote if (this._state.matchesCount >= MATCHES_LIMIT) { matchesCount += '+'; } - let matchesPosition: string = this._currentMatch < 0 ? '?' : String((this._currentMatch + 1)); + let matchesPosition: string = this._findModel.currentMatch < 0 ? '?' : String((this._findModel.currentMatch + 1)); label = strings.format(NLS_MATCHES_LOCATION, matchesPosition, matchesCount); } else { label = NLS_NO_RESULTS; @@ -412,18 +263,12 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote // TODO@rebornix, aria for `cell ${index}, line {line}` return localize('ariaSearchNoResultWithLineNumNoCurrentMatch', "{0} found for '{1}'", label, searchString); } - - clear() { - this._currentMatch = -1; - this._findMatches = []; - } - override dispose() { this._notebookEditor?.removeClassName(FIND_SHOW_TRANSITION); this._notebookEditor?.removeClassName(FIND_HIDE_TRANSITION); + this._findModel.dispose(); super.dispose(); } - } registerNotebookContribution(NotebookFindWidget.id, NotebookFindWidget); @@ -481,7 +326,7 @@ registerAction2(class extends Action2 { } }); -StartFindAction.addImplementation(100, (accessor: ServicesAccessor, args: any) => { +StartFindAction.addImplementation(100, (accessor: ServicesAccessor, codeEditor: ICodeEditor, args: any) => { const editorService = accessor.get(IEditorService); const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); @@ -494,7 +339,7 @@ StartFindAction.addImplementation(100, (accessor: ServicesAccessor, args: any) = return true; }); -StartFindReplaceAction.addImplementation(100, (accessor: ServicesAccessor, args: any) => { +StartFindReplaceAction.addImplementation(100, (accessor: ServicesAccessor, codeEditor: ICodeEditor, args: any) => { const editorService = accessor.get(IEditorService); const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts new file mode 100644 index 0000000000..89083b6db4 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts @@ -0,0 +1,340 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { INotebookEditor, CellFindMatch, CellEditState, CellFindMatchWithIndex } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import { FindDecorations } from 'vs/editor/contrib/find/findDecorations'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { IModelDeltaDecoration } from 'vs/editor/common/model'; +import { ICellModelDeltaDecorations, ICellModelDecorations } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; +import { FindReplaceState } from 'vs/editor/contrib/find/findState'; +import { CellKind, INotebookSearchOptions, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { findFirstInSorted } from 'vs/base/common/arrays'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; + + +export class FindModel extends Disposable { + private _findMatches: CellFindMatch[] = []; + protected _findMatchesStarts: PrefixSumComputer | null = null; + private _currentMatch: number = -1; + private _allMatchesDecorations: ICellModelDecorations[] = []; + private _currentMatchDecorations: ICellModelDecorations[] = []; + private _modelDisposable = new DisposableStore(); + + get findMatches() { + return this._findMatches; + } + + get currentMatch() { + return this._currentMatch; + } + + constructor( + private readonly _notebookEditor: INotebookEditor, + private readonly _state: FindReplaceState, + @IConfigurationService private readonly _configurationService: IConfigurationService + ) { + super(); + + this._register(_state.onFindReplaceStateChange(e => { + if (e.searchString || (e.isRevealed && this._state.isRevealed)) { + this.research(); + } + + if (e.isRevealed && !this._state.isRevealed) { + this.clear(); + } + })); + + this._register(this._notebookEditor.onDidChangeModel(e => { + this._registerModelListener(e); + })); + + if (this._notebookEditor.hasModel()) { + this._registerModelListener(this._notebookEditor.textModel); + } + } + + ensureFindMatches() { + if (!this._findMatchesStarts) { + this.set(this._findMatches, true); + } + } + + getCurrentMatch() { + const nextIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); + const cell = this._findMatches[nextIndex.index].cell; + const match = this._findMatches[nextIndex.index].matches[nextIndex.remainder]; + + return { + cell, + match + }; + } + + find(previous: boolean) { + if (!this.findMatches.length) { + return; + } + + // let currCell; + if (!this._findMatchesStarts) { + this.set(this._findMatches, true); + } else { + // const currIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); + // currCell = this._findMatches[currIndex.index].cell; + const totalVal = this._findMatchesStarts.getTotalValue(); + if (this._currentMatch === -1) { + this._currentMatch = previous ? totalVal - 1 : 0; + } else { + const nextVal = (this._currentMatch + (previous ? -1 : 1) + totalVal) % totalVal; + this._currentMatch = nextVal; + } + } + + const nextIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); + // const newFocusedCell = this._findMatches[nextIndex.index].cell; + this.setCurrentFindMatchDecoration(nextIndex.index, nextIndex.remainder); + this.revealCellRange(nextIndex.index, nextIndex.remainder); + + this._state.changeMatchInfo( + this._currentMatch, + this._findMatches.reduce((p, c) => p + c.matches.length, 0), + undefined + ); + } + + private revealCellRange(cellIndex: number, matchIndex: number) { + this._findMatches[cellIndex].cell.updateEditState(CellEditState.Editing, 'find'); + this._notebookEditor.focusElement(this._findMatches[cellIndex].cell); + this._notebookEditor.setCellEditorSelection(this._findMatches[cellIndex].cell, this._findMatches[cellIndex].matches[matchIndex].range); + this._notebookEditor.revealRangeInCenterIfOutsideViewportAsync(this._findMatches[cellIndex].cell, this._findMatches[cellIndex].matches[matchIndex].range); + } + + private _registerModelListener(notebookTextModel?: NotebookTextModel) { + this._modelDisposable.clear(); + + if (notebookTextModel) { + this._modelDisposable.add(notebookTextModel.onDidChangeContent((e) => { + if (!e.rawEvents.some(event => event.kind === NotebookCellsChangeType.ChangeCellContent || event.kind === NotebookCellsChangeType.ModelChange)) { + return; + } + + this.research(); + })); + } + + this.research(); + } + + research() { + if (!this._state.isRevealed) { + this.set([], false); + return; + } + + const findMatches = this._getFindMatches(); + if (!findMatches) { + return; + } + + if (this._currentMatch === -1) { + // no active current match + this.set(findMatches, false); + return; + } + + const oldCurrIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); + const oldCurrCell = this._findMatches[oldCurrIndex.index].cell; + const oldCurrMatchCellIndex = this._notebookEditor.viewModel!.getCellIndex(oldCurrCell); + + if (oldCurrMatchCellIndex < 0) { + // the cell containing the active match is deleted + const focusedCell = this._notebookEditor.viewModel!.viewCells[this._notebookEditor.viewModel!.getFocus().start]; + + if (!focusedCell) { + this.set(findMatches, false); + return; + } + + const matchAfterSelection = findFirstInSorted(findMatches.map(match => match.index), index => index >= oldCurrMatchCellIndex); + this._updateCurrentMatch(findMatches, this._matchesCountBeforeIndex(findMatches, matchAfterSelection)); + return; + } + + // the cell still exist + const cell = this._notebookEditor.viewModel!.viewCells[oldCurrMatchCellIndex]; + if (cell.cellKind === CellKind.Markup && cell.getEditState() === CellEditState.Preview) { + // find the nearest match above this cell + const matchAfterSelection = findFirstInSorted(findMatches.map(match => match.index), index => index >= oldCurrMatchCellIndex); + this._updateCurrentMatch(findMatches, this._matchesCountBeforeIndex(findMatches, matchAfterSelection)); + return; + } + + if ((cell.cellKind === CellKind.Markup && cell.getEditState() === CellEditState.Editing) || cell.cellKind === CellKind.Code) { + // check if there is monaco editor selection and find the first match, otherwise find the first match above current cell + // this._findMatches[cellIndex].matches[matchIndex].range + const currentMatchDecorationId = this._currentMatchDecorations.find(decoration => decoration.ownerId === cell.handle); + + if (currentMatchDecorationId) { + const currMatchRangeInEditor = (cell.editorAttached && currentMatchDecorationId.decorations[0] ? cell.getCellDecorationRange(currentMatchDecorationId.decorations[0]) : null) + ?? this._findMatches[oldCurrIndex.index].matches[oldCurrIndex.remainder].range; + + // not attached, just use the range + const matchAfterSelection = findFirstInSorted(findMatches, match => match.index >= oldCurrMatchCellIndex); + if (findMatches[matchAfterSelection].index > oldCurrMatchCellIndex) { + // there is no search result in curr cell anymore + this._updateCurrentMatch(findMatches, this._matchesCountBeforeIndex(findMatches, matchAfterSelection)); + } else { + // findMatches[matchAfterSelection].index === currMatchCellIndex + const cellMatch = findMatches[matchAfterSelection]; + const matchAfterOldSelection = findFirstInSorted(cellMatch.matches, match => Range.compareRangesUsingStarts(match.range, currMatchRangeInEditor) >= 0); + this._updateCurrentMatch(findMatches, this._matchesCountBeforeIndex(findMatches, matchAfterSelection) + matchAfterOldSelection); + } + } else { + const matchAfterSelection = findFirstInSorted(findMatches.map(match => match.index), index => index >= oldCurrMatchCellIndex); + this._updateCurrentMatch(findMatches, this._matchesCountBeforeIndex(findMatches, matchAfterSelection)); + } + + return; + } + + this.set(findMatches, false); + } + + private set(cellFindMatches: CellFindMatch[] | null, autoStart: boolean): void { + if (!cellFindMatches || !cellFindMatches.length) { + this._findMatches = []; + this.setAllFindMatchesDecorations([]); + + this.constructFindMatchesStarts(); + this._currentMatch = -1; + this.clearCurrentFindMatchDecoration(); + return; + } + + // all matches + this._findMatches = cellFindMatches; + this.setAllFindMatchesDecorations(cellFindMatches || []); + + // current match + this.constructFindMatchesStarts(); + + if (autoStart) { + this._currentMatch = 0; + this.setCurrentFindMatchDecoration(0, 0); + } + + this._state.changeMatchInfo( + this._currentMatch, + this._findMatches.reduce((p, c) => p + c.matches.length, 0), + undefined + ); + } + + private _getFindMatches(): CellFindMatchWithIndex[] | null { + const val = this._state.searchString; + const wordSeparators = this._configurationService.inspect('editor.wordSeparators').value; + + const options: INotebookSearchOptions = { regex: this._state.isRegex, wholeWord: this._state.wholeWord, caseSensitive: this._state.matchCase, wordSeparators: wordSeparators }; + if (!val) { + return null; + } + + const findMatches = this._notebookEditor.viewModel!.find(val, options).filter(match => match.matches.length > 0); + return findMatches; + } + + private _updateCurrentMatch(findMatches: CellFindMatchWithIndex[], currentMatchesPosition: number) { + this.set(findMatches, false); + this._currentMatch = currentMatchesPosition; + const nextIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); + this.setCurrentFindMatchDecoration(nextIndex.index, nextIndex.remainder); + + this._state.changeMatchInfo( + this._currentMatch, + this._findMatches.reduce((p, c) => p + c.matches.length, 0), + undefined + ); + } + + private _matchesCountBeforeIndex(findMatches: CellFindMatchWithIndex[], index: number) { + let prevMatchesCount = 0; + for (let i = 0; i < index; i++) { + prevMatchesCount += findMatches[i].matches.length; + } + + return prevMatchesCount; + } + + private constructFindMatchesStarts() { + if (this._findMatches && this._findMatches.length) { + const values = new Uint32Array(this._findMatches.length); + for (let i = 0; i < this._findMatches.length; i++) { + values[i] = this._findMatches[i].matches.length; + } + + this._findMatchesStarts = new PrefixSumComputer(values); + } else { + this._findMatchesStarts = null; + } + } + + private setCurrentFindMatchDecoration(cellIndex: number, matchIndex: number) { + this._notebookEditor.changeModelDecorations(accessor => { + const findMatchesOptions: ModelDecorationOptions = FindDecorations._CURRENT_FIND_MATCH_DECORATION; + + const cell = this._findMatches[cellIndex].cell; + const match = this._findMatches[cellIndex].matches[matchIndex]; + const decorations: IModelDeltaDecoration[] = [ + { range: match.range, options: findMatchesOptions } + ]; + const deltaDecoration: ICellModelDeltaDecorations = { + ownerId: cell.handle, + decorations: decorations + }; + + this._currentMatchDecorations = accessor.deltaDecorations(this._currentMatchDecorations, [deltaDecoration]); + }); + } + + private clearCurrentFindMatchDecoration() { + this._notebookEditor.changeModelDecorations(accessor => { + this._currentMatchDecorations = accessor.deltaDecorations(this._currentMatchDecorations, []); + }); + } + + private setAllFindMatchesDecorations(cellFindMatches: CellFindMatch[]) { + this._notebookEditor.changeModelDecorations((accessor) => { + + const findMatchesOptions: ModelDecorationOptions = FindDecorations._FIND_MATCH_DECORATION; + + const deltaDecorations: ICellModelDeltaDecorations[] = cellFindMatches.map(cellFindMatch => { + const findMatches = cellFindMatch.matches; + + // Find matches + const newFindMatchesDecorations: IModelDeltaDecoration[] = new Array(findMatches.length); + for (let i = 0, len = findMatches.length; i < len; i++) { + newFindMatchesDecorations[i] = { + range: findMatches[i].range, + options: findMatchesOptions + }; + } + + return { ownerId: cellFindMatch.cell.handle, decorations: newFindMatchesDecorations }; + }); + + this._allMatchesDecorations = accessor.deltaDecorations(this._allMatchesDecorations, deltaDecorations); + }); + } + + + clear() { + this.set([], false); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/test/find.test.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/test/find.test.ts new file mode 100644 index 0000000000..d6bef6d520 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/test/find.test.ts @@ -0,0 +1,195 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Range } from 'vs/editor/common/core/range'; +import { ITextBuffer, ValidAnnotatedEditOperation } from 'vs/editor/common/model'; +import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/model/wordHelper'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { FindReplaceState } from 'vs/editor/contrib/find/findState'; +import { IConfigurationService, IConfigurationValue } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { FindModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/findModel'; +import { IActiveNotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICellModelDecorations, ICellModelDeltaDecorations } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { CellEditType, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { TestCell, withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; + +suite('Notebook Find', () => { + const configurationValue: IConfigurationValue = { + value: USUAL_WORD_SEPARATORS + }; + const configurationService = new class extends TestConfigurationService { + override inspect() { + return configurationValue; + } + }(); + + const setupEditorForTest = (editor: IActiveNotebookEditor) => { + editor.changeModelDecorations = (callback) => { + return callback({ + deltaDecorations: (oldDecorations: ICellModelDecorations[], newDecorations: ICellModelDeltaDecorations[]) => { + const ret: ICellModelDecorations[] = []; + newDecorations.forEach(dec => { + const cell = editor.viewModel.viewCells.find(cell => cell.handle === dec.ownerId); + const decorations = cell?.deltaModelDecorations([], dec.decorations) ?? []; + + if (decorations.length > 0) { + ret.push({ ownerId: dec.ownerId, decorations: decorations }); + } + }); + + return ret; + } + }); + }; + }; + + test('Update find matches basics', async function () { + await withTestNotebook( + [ + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 2', 'markdown', CellKind.Markup, [], {}], + ], + async (editor, accessor) => { + accessor.stub(IConfigurationService, configurationService); + const state = new FindReplaceState(); + const model = new FindModel(editor, state, accessor.get(IConfigurationService)); + state.change({ isRevealed: true }, true); + state.change({ searchString: '1' }, true); + assert.strictEqual(model.findMatches.length, 2); + assert.strictEqual(model.currentMatch, -1); + model.find(false); + assert.strictEqual(model.currentMatch, 0); + model.find(false); + assert.strictEqual(model.currentMatch, 1); + model.find(false); + assert.strictEqual(model.currentMatch, 0); + + assert.strictEqual(editor.textModel.length, 3); + + editor.textModel.applyEdits([{ + editType: CellEditType.Replace, index: 3, count: 0, cells: [ + new TestCell(editor.viewModel.viewType, 3, '# next paragraph 1', 'markdown', CellKind.Code, [], accessor.get(IModeService)), + ] + }], true, undefined, () => undefined, undefined, true); + assert.strictEqual(editor.textModel.length, 4); + assert.strictEqual(model.findMatches.length, 3); + assert.strictEqual(model.currentMatch, 0); + }); + }); + + test('Update find matches basics 2', async function () { + await withTestNotebook( + [ + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1.1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1.2', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1.3', 'markdown', CellKind.Markup, [], {}], + ['paragraph 2', 'markdown', CellKind.Markup, [], {}], + ], + async (editor, accessor) => { + setupEditorForTest(editor); + accessor.stub(IConfigurationService, configurationService); + const state = new FindReplaceState(); + const model = new FindModel(editor, state, accessor.get(IConfigurationService)); + state.change({ isRevealed: true }, true); + state.change({ searchString: '1' }, true); + // find matches is not necessarily find results + assert.strictEqual(model.findMatches.length, 4); + assert.strictEqual(model.currentMatch, -1); + model.find(false); + assert.strictEqual(model.currentMatch, 0); + model.find(false); + assert.strictEqual(model.currentMatch, 1); + model.find(false); + assert.strictEqual(model.currentMatch, 2); + + editor.textModel.applyEdits([{ + editType: CellEditType.Replace, index: 2, count: 1, cells: [] + }], true, undefined, () => undefined, undefined, true); + assert.strictEqual(model.findMatches.length, 3); + + assert.strictEqual(model.currentMatch, 2); + model.find(true); + assert.strictEqual(model.currentMatch, 1); + model.find(false); + assert.strictEqual(model.currentMatch, 2); + model.find(false); + assert.strictEqual(model.currentMatch, 3); + model.find(false); + assert.strictEqual(model.currentMatch, 0); + }); + }); + + test('Update find matches basics 3', async function () { + await withTestNotebook( + [ + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1.1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1.2', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1.3', 'markdown', CellKind.Markup, [], {}], + ['paragraph 2', 'markdown', CellKind.Markup, [], {}], + ], + async (editor, accessor) => { + setupEditorForTest(editor); + accessor.stub(IConfigurationService, configurationService); + const state = new FindReplaceState(); + const model = new FindModel(editor, state, accessor.get(IConfigurationService)); + state.change({ isRevealed: true }, true); + state.change({ searchString: '1' }, true); + // find matches is not necessarily find results + assert.strictEqual(model.findMatches.length, 4); + assert.strictEqual(model.currentMatch, -1); + model.find(true); + assert.strictEqual(model.currentMatch, 4); + + editor.textModel.applyEdits([{ + editType: CellEditType.Replace, index: 2, count: 1, cells: [] + }], true, undefined, () => undefined, undefined, true); + assert.strictEqual(model.findMatches.length, 3); + assert.strictEqual(model.currentMatch, 3); + model.find(false); + assert.strictEqual(model.currentMatch, 0); + model.find(true); + assert.strictEqual(model.currentMatch, 3); + model.find(true); + assert.strictEqual(model.currentMatch, 2); + }); + }); + + test('Update find matches, #112748', async function () { + await withTestNotebook( + [ + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1.1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1.2', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1.3', 'markdown', CellKind.Markup, [], {}], + ['paragraph 2', 'markdown', CellKind.Markup, [], {}], + ], + async (editor, accessor) => { + setupEditorForTest(editor); + accessor.stub(IConfigurationService, configurationService); + const state = new FindReplaceState(); + const model = new FindModel(editor, state, accessor.get(IConfigurationService)); + state.change({ isRevealed: true }, true); + state.change({ searchString: '1' }, true); + // find matches is not necessarily find results + assert.strictEqual(model.findMatches.length, 4); + assert.strictEqual(model.currentMatch, -1); + model.find(false); + model.find(false); + model.find(false); + assert.strictEqual(model.currentMatch, 2); + (editor.viewModel.viewCells[1].textBuffer as ITextBuffer).applyEdits([ + new ValidAnnotatedEditOperation(null, new Range(1, 1, 1, 14), '', false, false, false) + ], false, true); + // cell content updates, recompute + model.research(); + assert.strictEqual(model.currentMatch, 1); + }); + }); +}); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts b/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts index 41f4897099..ea71767ea2 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts @@ -38,8 +38,8 @@ export class FoldingController extends Disposable implements INotebookEditorCont return; } - this._localStore.add(this._notebookEditor.viewModel.eventDispatcher.onDidChangeCellState(e => { - if (e.source.editStateChanged && e.cell.cellKind === CellKind.Markdown) { + this._localStore.add(this._notebookEditor.viewModel.viewContext.eventDispatcher.onDidChangeCellState(e => { + if (e.source.editStateChanged && e.cell.cellKind === CellKind.Markup) { this._foldingModel?.recompute(); // this._updateEditorFoldingRanges(); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts b/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts index 687ec42ee4..63d8a49157 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts @@ -16,13 +16,13 @@ suite('Notebook Folding', () => { test('Folding based on markdown cells', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.1', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], + ['## header 2.1', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], (editor) => { const viewModel = editor.viewModel; @@ -43,13 +43,13 @@ suite('Notebook Folding', () => { test('Top level header in a cell wins', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.1\n# header3', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], + ['## header 2.1\n# header3', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], (editor) => { const viewModel = editor.viewModel; @@ -75,13 +75,13 @@ suite('Notebook Folding', () => { test('Folding', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.1', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], + ['## header 2.1', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], (editor) => { const viewModel = editor.viewModel; @@ -97,13 +97,13 @@ suite('Notebook Folding', () => { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], + ['## header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], (editor) => { const viewModel = editor.viewModel; @@ -120,13 +120,13 @@ suite('Notebook Folding', () => { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], (editor) => { const viewModel = editor.viewModel; @@ -145,13 +145,13 @@ suite('Notebook Folding', () => { test('Nested Folding', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], (editor) => { const viewModel = editor.viewModel; @@ -200,18 +200,18 @@ suite('Notebook Folding', () => { test('Folding Memento', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], (editor) => { const viewModel = editor.viewModel; @@ -229,18 +229,18 @@ suite('Notebook Folding', () => { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], (editor) => { const viewModel = editor.viewModel; @@ -262,18 +262,18 @@ suite('Notebook Folding', () => { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], (editor) => { const viewModel = editor.viewModel; @@ -297,18 +297,18 @@ suite('Notebook Folding', () => { test('View Index', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], (editor) => { const viewModel = editor.viewModel; @@ -334,18 +334,18 @@ suite('Notebook Folding', () => { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], (editor) => { const viewModel = editor.viewModel; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/gettingStarted/notebookGettingStarted.ts b/src/vs/workbench/contrib/notebook/browser/contrib/gettingStarted/notebookGettingStarted.ts new file mode 100644 index 0000000000..e3080b9eee --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/gettingStarted/notebookGettingStarted.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { localize } from 'vs/nls'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { CATEGORIES } from 'vs/workbench/common/actions'; +import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { Memento } from 'vs/workbench/common/memento'; +import { HAS_OPENED_NOTEBOOK } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; + +const hasOpenedNotebookKey = 'hasOpenedNotebook'; +const hasShownGettingStartedKey = 'hasShownNotebookGettingStarted'; + +const showGettingStartedSetting = 'notebook.experimental.openGettingStarted'; + +/** + * Sets a context key when a notebook has ever been opened by the user + */ +export class NotebookGettingStarted extends Disposable implements IWorkbenchContribution { + + constructor( + @IEditorService _editorService: IEditorService, + @IStorageService _storageService: IStorageService, + @IContextKeyService _contextKeyService: IContextKeyService, + @ICommandService _commandService: ICommandService, + @IConfigurationService _configurationService: IConfigurationService, + ) { + super(); + + const hasOpenedNotebook = HAS_OPENED_NOTEBOOK.bindTo(_contextKeyService); + const memento = new Memento('notebookGettingStarted2', _storageService); + const storedValue = memento.getMemento(StorageScope.GLOBAL, StorageTarget.USER); + if (storedValue[hasOpenedNotebookKey]) { + hasOpenedNotebook.set(true); + } + + const needToShowGettingStarted = _configurationService.getValue(showGettingStartedSetting) && !storedValue[hasShownGettingStartedKey]; + if (!storedValue[hasOpenedNotebookKey] || needToShowGettingStarted) { + const onDidOpenNotebook = () => { + hasOpenedNotebook.set(true); + storedValue[hasOpenedNotebookKey] = true; + + if (needToShowGettingStarted) { + _commandService.executeCommand('workbench.action.openWalkthrough', { category: 'notebooks', step: 'notebookProfile' }, true); + storedValue[hasShownGettingStartedKey] = true; + } + + memento.saveMemento(); + }; + + if (_editorService.activeEditor?.typeId === NotebookEditorInput.ID) { + // active editor is notebook + onDidOpenNotebook(); + return; + } + + const listener = this._register(_editorService.onDidActiveEditorChange(() => { + if (_editorService.activeEditor?.typeId === NotebookEditorInput.ID) { + listener.dispose(); + onDidOpenNotebook(); + } + })); + } + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(NotebookGettingStarted, LifecyclePhase.Restored); + +registerAction2(class NotebookClearNotebookLayoutAction extends Action2 { + constructor() { + super({ + id: 'workbench.notebook.layout.gettingStarted', + title: localize('workbench.notebook.layout.gettingStarted.label', "Reset notebook getting started"), + f1: true, + category: CATEGORIES.Developer, + }); + } + run(accessor: ServicesAccessor): void { + const storageService = accessor.get(IStorageService); + const memento = new Memento('notebookGettingStarted', storageService); + + const storedValue = memento.getMemento(StorageScope.GLOBAL, StorageTarget.USER); + storedValue[hasOpenedNotebookKey] = undefined; + memento.saveMemento(); + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/layout/layoutActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/layout/layoutActions.ts index b9cadaff96..a227b7bae6 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/layout/layoutActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/layout/layoutActions.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; @@ -8,7 +8,7 @@ import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/act import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { INotebookActionContext, NOTEBOOK_ACTIONS_CATEGORY } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; -import { CellToolbarLocKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellToolbarLocation } from 'vs/workbench/contrib/notebook/common/notebookCommon'; const TOGGLE_CELL_TOOLBAR_POSITION = 'notebook.toggleCellToolbarPosition'; @@ -33,9 +33,9 @@ export class ToggleCellToolbarPositionAction extends Action2 { // from toolbar const viewType = editor.viewModel.viewType; const configurationService = accessor.get(IConfigurationService); - const toolbarPosition = configurationService.getValue(CellToolbarLocKey); + const toolbarPosition = configurationService.getValue(CellToolbarLocation); const newConfig = this.togglePosition(viewType, toolbarPosition); - await configurationService.updateValue(CellToolbarLocKey, newConfig); + await configurationService.updateValue(CellToolbarLocation, newConfig); } } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/layout/test/layoutActions.test.ts b/src/vs/workbench/contrib/notebook/browser/contrib/layout/test/layoutActions.test.ts index a35bee7970..5140831fc5 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/layout/test/layoutActions.test.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/layout/test/layoutActions.test.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts b/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts index 703fa826ea..640d548ed0 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; @@ -68,7 +68,7 @@ registerAction2(class extends NotebookCellAction { return; } - const newFocusMode = newCell.cellKind === CellKind.Markdown && newCell.getEditState() === CellEditState.Preview ? 'container' : 'editor'; + const newFocusMode = newCell.cellKind === CellKind.Markup && newCell.getEditState() === CellEditState.Preview ? 'container' : 'editor'; editor.focusNotebookCell(newCell, newFocusMode); editor.cursorNavigationMode = true; } @@ -115,7 +115,7 @@ registerAction2(class extends NotebookCellAction { return; } - const newFocusMode = newCell.cellKind === CellKind.Markdown && newCell.getEditState() === CellEditState.Preview ? 'container' : 'editor'; + const newFocusMode = newCell.cellKind === CellKind.Markup && newCell.getEditState() === CellEditState.Preview ? 'container' : 'editor'; editor.focusNotebookCell(newCell, newFocusMode); editor.cursorNavigationMode = true; } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts index 8fddec5b15..ce658eb122 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -396,7 +396,7 @@ export class NotebookCellOutline implements IOutline { for (let i = 0; i < viewModel.length; i++) { const cell = viewModel.viewCells[i]; - const isMarkdown = cell.cellKind === CellKind.Markdown; + const isMarkdown = cell.cellKind === CellKind.Markup; if (!isMarkdown && !includeCodeCells) { continue; } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/test/notebookOutline.test.ts b/src/vs/workbench/contrib/notebook/browser/contrib/outline/test/notebookOutline.test.ts index f1e7fe845b..359421b09b 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/outline/test/notebookOutline.test.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/test/notebookOutline.test.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; @@ -49,7 +49,7 @@ suite('Notebook Outline', function () { test('special characters in heading', async function () { await withNotebookOutline([ - ['# Hellö & Hällo', 'md', CellKind.Markdown] + ['# Hellö & Hällo', 'md', CellKind.Markup] ], outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); @@ -57,7 +57,7 @@ suite('Notebook Outline', function () { }); await withNotebookOutline([ - ['# bold', 'md', CellKind.Markdown] + ['# bold', 'md', CellKind.Markup] ], outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); @@ -67,7 +67,7 @@ suite('Notebook Outline', function () { test('Heading text defines entry label', async function () { return await withNotebookOutline([ - ['foo\n # h1', 'md', CellKind.Markdown] + ['foo\n # h1', 'md', CellKind.Markup] ], outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); @@ -77,7 +77,7 @@ suite('Notebook Outline', function () { test('Notebook outline ignores markdown headings #115200', async function () { await withNotebookOutline([ - ['## h2 \n# h1', 'md', CellKind.Markdown] + ['## h2 \n# h1', 'md', CellKind.Markup] ], outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 2); @@ -86,8 +86,8 @@ suite('Notebook Outline', function () { }); await withNotebookOutline([ - ['## h2', 'md', CellKind.Markdown], - ['# h1', 'md', CellKind.Markdown] + ['## h2', 'md', CellKind.Markup], + ['# h1', 'md', CellKind.Markup] ], outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 2); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/profile/notebookProfile.ts b/src/vs/workbench/contrib/notebook/browser/contrib/profile/notebookProfile.ts new file mode 100644 index 0000000000..fe7d59325d --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/profile/notebookProfile.ts @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Registry } from 'vs/platform/registry/common/platform'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { localize } from 'vs/nls'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { optional } from 'vs/platform/instantiation/common/instantiation'; +import { CellToolbarLocation, CompactView, ConsolidatedRunButton, FocusIndicator, GlobalToolbar, InsertToolbarLocation, ShowCellStatusBar, UndoRedoPerCell } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; +import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; + +export enum NotebookProfileType { + default = 'default', + jupyter = 'jupyter', + colab = 'colab' +} + +const profiles = { + [NotebookProfileType.default]: { + [FocusIndicator]: 'gutter', + [InsertToolbarLocation]: 'both', + [GlobalToolbar]: true, + [CellToolbarLocation]: { default: 'right' }, + [CompactView]: true, + [ShowCellStatusBar]: 'visible', + [ConsolidatedRunButton]: true, + [UndoRedoPerCell]: false + }, + [NotebookProfileType.jupyter]: { + [FocusIndicator]: 'gutter', + [InsertToolbarLocation]: 'notebookToolbar', + [GlobalToolbar]: true, + [CellToolbarLocation]: { default: 'left' }, + [CompactView]: true, + [ShowCellStatusBar]: 'visible', + [ConsolidatedRunButton]: false, + [UndoRedoPerCell]: true + }, + [NotebookProfileType.colab]: { + [FocusIndicator]: 'border', + [InsertToolbarLocation]: 'betweenCells', + [GlobalToolbar]: false, + [CellToolbarLocation]: { default: 'right' }, + [CompactView]: false, + [ShowCellStatusBar]: 'hidden', + [ConsolidatedRunButton]: true, + [UndoRedoPerCell]: false + } +}; + +async function applyProfile(configService: IConfigurationService, profile: Record): Promise { + const promises = []; + for (let settingKey in profile) { + promises.push(configService.updateValue(settingKey, profile[settingKey])); + } + + await Promise.all(promises); +} + +export interface ISetProfileArgs { + profile: NotebookProfileType; +} + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'notebook.setProfile', + title: localize('setProfileTitle', "Set Profile") + }); + } + + async run(accessor: ServicesAccessor, args: unknown): Promise { + if (!isSetProfileArgs(args)) { + return; + } + + const configService = accessor.get(IConfigurationService); + return applyProfile(configService, profiles[args.profile]); + } +}); + +function isSetProfileArgs(args: unknown): args is ISetProfileArgs { + const setProfileArgs = args as ISetProfileArgs; + return setProfileArgs.profile === NotebookProfileType.colab || + setProfileArgs.profile === NotebookProfileType.default || + setProfileArgs.profile === NotebookProfileType.jupyter; +} + +export class NotebookProfileContribution extends Disposable { + constructor(@IConfigurationService configService: IConfigurationService, @optional(ITASExperimentService) private readonly experimentService: ITASExperimentService) { + super(); + + if (this.experimentService) { + this.experimentService.getTreatment('notebookprofile').then(treatment => { + if (treatment === undefined) { + return; + } else { + // check if settings are already modified + const focusIndicator = configService.getValue(FocusIndicator); + const insertToolbarPosition = configService.getValue(InsertToolbarLocation); + const globalToolbar = configService.getValue(GlobalToolbar); + // const cellToolbarLocation = configService.getValue(CellToolbarLocation); + const compactView = configService.getValue(CompactView); + const showCellStatusBar = configService.getValue(ShowCellStatusBar); + const consolidatedRunButton = configService.getValue(ConsolidatedRunButton); + if (focusIndicator === 'border' + && insertToolbarPosition === 'both' + && globalToolbar === false + // && cellToolbarLocation === undefined + && compactView === true + && showCellStatusBar === 'visible' + && consolidatedRunButton === true + ) { + applyProfile(configService, profiles[treatment] ?? profiles[NotebookProfileType.default]); + } + } + }); + } + } +} + +const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); +workbenchContributionsRegistry.registerWorkbenchContribution(NotebookProfileContribution, LifecyclePhase.Ready); + 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 618e0998d0..3df3ac9afc 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts @@ -5,33 +5,56 @@ import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IQuickInputButton, IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { NOTEBOOK_ACTIONS_CATEGORY } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; -import { getNotebookEditorFromEditorPane, INotebookEditor, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NOTEBOOK_ACTIONS_CATEGORY, SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { getNotebookEditorFromEditorPane, INotebookEditor, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { combinedDisposable, Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/common/statusbar'; import { configureKernelIcon, selectKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; -import { INotebookKernel, INotebookTextModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookKernel, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ILabelService } from 'vs/platform/label/common/label'; +import { ILogService } from 'vs/platform/log/common/log'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { HoverProviderRegistry } from 'vs/editor/common/modes'; +import { Schemas } from 'vs/base/common/network'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; registerAction2(class extends Action2 { constructor() { super({ - id: 'notebook.selectKernel', + id: SELECT_KERNEL_ID, category: NOTEBOOK_ACTIONS_CATEGORY, title: { value: nls.localize('notebookActions.selectKernel', "Select Notebook Kernel"), original: 'Select Notebook Kernel' }, precondition: NOTEBOOK_IS_ACTIVE_EDITOR, icon: selectKernelIcon, f1: true, + menu: [{ + id: MenuId.EditorTitle, + when: ContextKeyExpr.and( + NOTEBOOK_IS_ACTIVE_EDITOR, + NOTEBOOK_KERNEL_COUNT.notEqualsTo(0), + ContextKeyExpr.notEquals('config.notebook.globalToolbar', true) + ), + group: 'navigation', + order: -10 + }, { + id: MenuId.NotebookToolbar, + when: ContextKeyExpr.and( + NOTEBOOK_KERNEL_COUNT.notEqualsTo(0), + ContextKeyExpr.equals('config.notebook.globalToolbar', true) + ), + group: 'status', + order: -10 + }], description: { description: nls.localize('notebookActions.selectKernel.args', "Notebook Kernel Args"), args: [ @@ -53,19 +76,19 @@ registerAction2(class extends Action2 { } ] }, - }); } - async run(accessor: ServicesAccessor, context?: { id: string, extension: string }): Promise { + async run(accessor: ServicesAccessor, context?: { id: string, extension: string }): Promise { const notebookKernelService = accessor.get(INotebookKernelService); const editorService = accessor.get(IEditorService); const quickInputService = accessor.get(IQuickInputService); const labelService = accessor.get(ILabelService); + const logService = accessor.get(ILogService); const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); if (!editor || !editor.hasModel()) { - return; + return false; } if (context && (typeof context.id !== 'string' || typeof context.extension !== 'string')) { @@ -78,7 +101,7 @@ registerAction2(class extends Action2 { if (selected && context && selected.id === context.id && ExtensionIdentifier.equals(selected.extension, context.extension)) { // current kernel is wanted kernel -> done - return; + return true; } let newKernel: INotebookKernel | undefined; @@ -90,6 +113,10 @@ registerAction2(class extends Action2 { break; } } + if (!newKernel) { + logService.warn(`wanted kernel DOES NOT EXIST, wanted: ${wantedId}, all: ${all.map(k => k.id)}`); + return false; + } } if (!newKernel) { @@ -132,19 +159,71 @@ registerAction2(class extends Action2 { if (newKernel) { notebookKernelService.selectKernelForNotebook(newKernel, notebook); + return true; } + return false; } }); + +class ImplictKernelSelector implements IDisposable { + + readonly dispose: () => void; + + constructor( + notebook: NotebookTextModel, + suggested: INotebookKernel, + @INotebookKernelService notebookKernelService: INotebookKernelService, + @ILogService logService: ILogService + ) { + const disposables = new DisposableStore(); + this.dispose = disposables.dispose.bind(disposables); + + const selectKernel = () => { + disposables.clear(); + notebookKernelService.selectKernelForNotebook(suggested, notebook); + }; + + // IMPLICITLY select a suggested kernel when the notebook has been changed + // e.g change cell source, move cells, etc + disposables.add(notebook.onDidChangeContent(e => { + for (let event of e.rawEvents) { + switch (event.kind) { + case NotebookCellsChangeType.ChangeCellContent: + case NotebookCellsChangeType.ModelChange: + case NotebookCellsChangeType.Move: + case NotebookCellsChangeType.ChangeLanguage: + logService.trace('IMPLICIT kernel selection because of change event', event.kind); + selectKernel(); + break; + } + } + })); + + + // IMPLICITLY select a suggested kernel when users start to hover. This should + // be a strong enough hint that the user wants to interact with the notebook. Maybe + // add more triggers like goto-providers or completion-providers + disposables.add(HoverProviderRegistry.register({ scheme: Schemas.vscodeNotebookCell, pattern: notebook.uri.path }, { + provideHover() { + logService.trace('IMPLICIT kernel selection because of hover'); + selectKernel(); + return undefined; + } + })); + } +} + export class KernelStatus extends Disposable implements IWorkbenchContribution { private readonly _editorDisposables = this._register(new DisposableStore()); - private readonly _kernelInfoElement = this._register(new MutableDisposable()); + private readonly _kernelInfoElement = this._register(new DisposableStore()); constructor( @IEditorService private readonly _editorService: IEditorService, @IStatusbarService private readonly _statusbarService: IStatusbarService, @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, + @ILogService private readonly _logService: ILogService, ) { super(); this._register(this._editorService.onDidActiveEditorChange(() => this._updateStatusbar())); @@ -161,6 +240,12 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { } const updateStatus = () => { + if (activeEditor.notebookOptions.getLayoutConfiguration().globalToolbar) { + // kernel info rendered in the notebook toolbar already + this._kernelInfoElement.clear(); + return; + } + const notebook = activeEditor.viewModel?.notebookDocument; if (notebook) { this._showKernelStatus(notebook); @@ -173,62 +258,68 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { this._editorDisposables.add(this._notebookKernelService.onDidChangeNotebookKernelBinding(updateStatus)); this._editorDisposables.add(this._notebookKernelService.onDidChangeNotebookAffinity(updateStatus)); this._editorDisposables.add(activeEditor.onDidChangeModel(updateStatus)); + this._editorDisposables.add(activeEditor.notebookOptions.onDidChangeOptions(updateStatus)); updateStatus(); } - private _showKernelStatus(notebook: INotebookTextModel) { + private _showKernelStatus(notebook: NotebookTextModel) { - let { selected, all } = this._notebookKernelService.getMatchingKernel(notebook); + this._kernelInfoElement.clear(); + + let { selected, suggested, all } = this._notebookKernelService.getMatchingKernel(notebook); let isSuggested = false; if (all.length === 0) { // no kernel -> no status - this._kernelInfoElement.clear(); return; - } else if (selected || all.length === 1) { + } else if (selected || suggested) { // selected or single kernel - if (!selected) { - selected = all[0]; + let kernel = selected; + + if (!kernel) { + // proceed with suggested kernel - show UI and install handler that selects the kernel + // when non trivial interactions with the notebook happen. + kernel = suggested!; isSuggested = true; + this._kernelInfoElement.add(new ImplictKernelSelector(notebook, kernel, this._notebookKernelService, this._logService)); } - const text = `$(notebook-kernel-select) ${selected.label}`; - const tooltip = selected.description ?? selected.detail ?? selected.label; - const registration = this._statusbarService.addEntry( + const tooltip = kernel.description ?? kernel.detail ?? kernel.label; + this._kernelInfoElement.add(this._statusbarService.addEntry( { - text, - ariaLabel: selected.label, + name: nls.localize('notebook.info', "Notebook Kernel Info"), + text: `$(notebook-kernel-select) ${kernel.label}`, + ariaLabel: kernel.label, tooltip: isSuggested ? nls.localize('tooltop', "{0} (suggestion)", tooltip) : tooltip, - command: 'notebook.selectKernel', + command: SELECT_KERNEL_ID, }, 'notebook.selectKernel', - nls.localize('notebook.info', "Notebook Kernel Info"), StatusbarAlignment.RIGHT, - 1000 - ); - const listener = selected.onDidChange(() => this._showKernelStatus(notebook)); - this._kernelInfoElement.value = combinedDisposable(listener, registration); + 10 + )); + + this._kernelInfoElement.add(kernel.onDidChange(() => this._showKernelStatus(notebook))); + } else { // multiple kernels -> show selection hint - const registration = this._statusbarService.addEntry( + this._kernelInfoElement.add(this._statusbarService.addEntry( { + name: nls.localize('notebook.select', "Notebook Kernel Selection"), text: nls.localize('kernel.select.label', "Select Kernel"), ariaLabel: nls.localize('kernel.select.label', "Select Kernel"), - command: 'notebook.selectKernel', + command: SELECT_KERNEL_ID, backgroundColor: { id: 'statusBarItem.prominentBackground' } }, 'notebook.selectKernel', - nls.localize('notebook.select', "Notebook Kernel Selection"), StatusbarAlignment.RIGHT, - 1000 - ); - this._kernelInfoElement.value = registration; + 10 + )); } } } -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(KernelStatus, LifecyclePhase.Ready); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(KernelStatus, LifecyclePhase.Restored); export class ActiveCellStatus extends Disposable implements IWorkbenchContribution { @@ -268,14 +359,14 @@ export class ActiveCellStatus extends Disposable implements IWorkbenchContributi return; } - const entry = { text: newText, ariaLabel: newText }; + const entry = { name: nls.localize('notebook.activeCellStatusName', "Notebook Editor Selections"), text: newText, ariaLabel: newText }; if (!this._accessor.value) { this._accessor.value = this._statusbarService.addEntry( entry, 'notebook.activeCellStatus', - nls.localize('notebook.activeCellStatusName', "Notebook Editor Selections"), StatusbarAlignment.RIGHT, - 100); + 100 + ); } else { this._accessor.value.update(entry); } @@ -289,10 +380,11 @@ export class ActiveCellStatus extends Disposable implements IWorkbenchContributi const idxFocused = vm.getCellIndex(activeCell) + 1; const numSelected = vm.getSelections().reduce((prev, range) => prev + (range.end - range.start), 0); + const totalCells = vm.getCells().length; return numSelected > 1 ? nls.localize('notebook.multiActiveCellIndicator', "Cell {0} ({1} selected)", idxFocused, numSelected) : - nls.localize('notebook.singleActiveCellIndicator', "Cell {0}", idxFocused); + nls.localize('notebook.singleActiveCellIndicator', "Cell {0} of {1}", idxFocused, totalCells); } } -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ActiveCellStatus, LifecyclePhase.Ready); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ActiveCellStatus, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/cellStatusBar.ts b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/cellStatusBar.ts deleted file mode 100644 index edce9fb60f..0000000000 --- a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/cellStatusBar.ts +++ /dev/null @@ -1,142 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { flatten } from 'vs/base/common/arrays'; -import { Throttler } from 'vs/base/common/async'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { ICellViewModel, INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; -import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; -import { INotebookCellStatusBarItemList } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { cellRangesToIndexes } from 'vs/workbench/contrib/notebook/common/notebookRange'; - -export class NotebookStatusBarController extends Disposable implements INotebookEditorContribution { - static id: string = 'workbench.notebook.statusBar'; - - private readonly _visibleCells = new Map(); - - private readonly _viewModelDisposables = new DisposableStore(); - - constructor( - private readonly _notebookEditor: INotebookEditor, - @INotebookCellStatusBarService private readonly _notebookCellStatusBarService: INotebookCellStatusBarService - ) { - super(); - this._updateVisibleCells(); - this._register(this._notebookEditor.onDidChangeVisibleRanges(this._updateVisibleCells, this)); - this._register(this._notebookEditor.onDidChangeModel(this._onModelChange, this)); - this._register(this._notebookCellStatusBarService.onDidChangeProviders(this._updateEverything, this)); - this._register(this._notebookCellStatusBarService.onDidChangeItems(this._updateEverything, this)); - } - - private _onModelChange() { - this._viewModelDisposables.clear(); - const vm = this._notebookEditor.viewModel; - if (!vm) { - return; - } - - this._viewModelDisposables.add(vm.onDidChangeViewCells(() => this._updateEverything())); - this._updateEverything(); - } - - private _updateEverything(): void { - this._visibleCells.forEach(cell => cell.dispose()); - this._visibleCells.clear(); - this._updateVisibleCells(); - } - - private _updateVisibleCells(): void { - const vm = this._notebookEditor.viewModel; - if (!vm) { - return; - } - - const newVisibleCells = new Set(); - const rangesWithEnd = this._notebookEditor.visibleRanges - .map(range => ({ start: range.start, end: range.end + 1 })); - cellRangesToIndexes(rangesWithEnd) - .map(index => vm.cellAt(index)) - .filter((cell: CellViewModel | undefined): cell is CellViewModel => !!cell) - .map(cell => { - if (!this._visibleCells.has(cell.handle)) { - const helper = new CellStatusBarHelper(vm, cell, this._notebookCellStatusBarService); - this._visibleCells.set(cell.handle, helper); - } - newVisibleCells.add(cell.handle); - }); - - for (let handle of this._visibleCells.keys()) { - if (!newVisibleCells.has(handle)) { - this._visibleCells.get(handle)?.dispose(); - this._visibleCells.delete(handle); - } - } - } - - override dispose(): void { - this._visibleCells.forEach(cell => cell.dispose()); - this._visibleCells.clear(); - } -} - -class CellStatusBarHelper extends Disposable { - private _currentItemIds: string[] = []; - private _currentItemLists: INotebookCellStatusBarItemList[] = []; - - private readonly _cancelTokenSource: CancellationTokenSource; - - private readonly _updateThrottler = new Throttler(); - - constructor( - private readonly _notebookViewModel: NotebookViewModel, - private readonly _cell: ICellViewModel, - private readonly _notebookCellStatusBarService: INotebookCellStatusBarService - ) { - super(); - - this._cancelTokenSource = new CancellationTokenSource(); - this._register(toDisposable(() => this._cancelTokenSource.dispose(true))); - - this._updateSoon(); - this._register(this._cell.model.onDidChangeContent(() => this._updateSoon())); - this._register(this._cell.model.onDidChangeLanguage(() => this._updateSoon())); - this._register(this._cell.model.onDidChangeMetadata(() => this._updateSoon())); - this._register(this._cell.model.onDidChangeOutputs(() => this._updateSoon())); - } - - private _updateSoon(): void { - this._updateThrottler.queue(() => this._update()); - } - - private async _update() { - const cellIndex = this._notebookViewModel.getCellIndex(this._cell); - const docUri = this._notebookViewModel.notebookDocument.uri; - const viewType = this._notebookViewModel.notebookDocument.viewType; - const itemLists = await this._notebookCellStatusBarService.getStatusBarItemsForCell(docUri, cellIndex, viewType, this._cancelTokenSource.token); - if (this._cancelTokenSource.token.isCancellationRequested) { - itemLists.forEach(itemList => itemList.dispose && itemList.dispose()); - return; - } - - const items = flatten(itemLists.map(itemList => itemList.items)); - const newIds = this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items }]); - - this._currentItemLists.forEach(itemList => itemList.dispose && itemList.dispose()); - this._currentItemLists = itemLists; - this._currentItemIds = newIds; - } - - override dispose() { - super.dispose(); - - this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items: [] }]); - this._currentItemLists.forEach(itemList => itemList.dispose && itemList.dispose()); - } -} - -registerNotebookContribution(NotebookStatusBarController.id, NotebookStatusBarController); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/contributedStatusBarItemController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/contributedStatusBarItemController.ts index 4358ff31a0..f3ab025cda 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/contributedStatusBarItemController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/contributedStatusBarItemController.ts @@ -1,10 +1,10 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { flatten } from 'vs/base/common/arrays'; -import { Throttler } from 'vs/base/common/async'; +import { disposableTimeout, Throttler } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { ICellVisibilityChangeEvent, NotebookVisibleCellObserver } from 'vs/workbench/contrib/notebook/browser/contrib/statusBar/notebookVisibleCellObserver'; @@ -15,7 +15,7 @@ import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/com import { INotebookCellStatusBarItemList } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export class ContributedStatusBarItemController extends Disposable implements INotebookEditorContribution { - static id: string = 'workbench.notebook.statusBar'; + static id: string = 'workbench.notebook.statusBar.contributed'; private readonly _visibleCells = new Map(); @@ -69,7 +69,7 @@ class CellStatusBarHelper extends Disposable { private _currentItemIds: string[] = []; private _currentItemLists: INotebookCellStatusBarItemList[] = []; - private readonly _cancelTokenSource: CancellationTokenSource; + private _activeToken: CancellationTokenSource | undefined; private readonly _updateThrottler = new Throttler(); @@ -80,29 +80,31 @@ class CellStatusBarHelper extends Disposable { ) { super(); - this._cancelTokenSource = new CancellationTokenSource(); - this._register(toDisposable(() => this._cancelTokenSource.dispose(true))); - + this._register(toDisposable(() => this._activeToken?.dispose(true))); this._updateSoon(); this._register(this._cell.model.onDidChangeContent(() => this._updateSoon())); this._register(this._cell.model.onDidChangeLanguage(() => this._updateSoon())); this._register(this._cell.model.onDidChangeMetadata(() => this._updateSoon())); + this._register(this._cell.model.onDidChangeInternalMetadata(() => this._updateSoon())); this._register(this._cell.model.onDidChangeOutputs(() => this._updateSoon())); } private _updateSoon(): void { // Wait a tick to make sure that the event is fired to the EH before triggering status bar providers - setTimeout(() => { + this._register(disposableTimeout(() => { this._updateThrottler.queue(() => this._update()); - }, 0); + }, 0)); } private async _update() { const cellIndex = this._notebookViewModel.getCellIndex(this._cell); const docUri = this._notebookViewModel.notebookDocument.uri; const viewType = this._notebookViewModel.notebookDocument.viewType; - const itemLists = await this._notebookCellStatusBarService.getStatusBarItemsForCell(docUri, cellIndex, viewType, this._cancelTokenSource.token); - if (this._cancelTokenSource.token.isCancellationRequested) { + + this._activeToken?.dispose(true); + const tokenSource = this._activeToken = new CancellationTokenSource(); + const itemLists = await this._notebookCellStatusBarService.getStatusBarItemsForCell(docUri, cellIndex, viewType, tokenSource.token); + if (tokenSource.token.isCancellationRequested) { itemLists.forEach(itemList => itemList.dispose && itemList.dispose()); return; } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/executionStatusBarItemController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/executionStatusBarItemController.ts index 0458d96470..a8e28fe359 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/executionStatusBarItemController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/executionStatusBarItemController.ts @@ -1,22 +1,26 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { RunOnceScheduler } from 'vs/base/common/async'; -import { Event } from 'vs/base/common/event'; import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { localize } from 'vs/nls'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { InputFocusedContext } from 'vs/platform/contextkey/common/contextkeys'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { ICellVisibilityChangeEvent, NotebookVisibleCellObserver } from 'vs/workbench/contrib/notebook/browser/contrib/statusBar/notebookVisibleCellObserver'; -import { ICellViewModel, INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { EXECUTE_CELL_COMMAND_ID, ICellViewModel, INotebookEditor, INotebookEditorContribution, NOTEBOOK_CELL_EXECUTION_STATE, QUIT_EDIT_CELL_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { cellStatusIconError, cellStatusIconSuccess } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellStatusbarAlignment, INotebookCellStatusBarItem, NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellStatusbarAlignment, INotebookCellStatusBarItem, NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export class NotebookStatusBarController extends Disposable implements INotebookEditorContribution { - static id: string = 'workbench.notebook.statusBar'; + static id: string = 'workbench.notebook.statusBar.exec'; private readonly _visibleCells = new Map(); @@ -24,6 +28,7 @@ export class NotebookStatusBarController extends Disposable implements INotebook constructor( private readonly _notebookEditor: INotebookEditor, + @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); this._observer = this._register(new NotebookVisibleCellObserver(this._notebookEditor)); @@ -46,8 +51,9 @@ export class NotebookStatusBarController extends Disposable implements INotebook for (let newCell of e.added) { const helpers = [ - new ExecutionStateCellStatusBarHelper(vm, newCell), - new TimerCellStatusBarHelper(vm, newCell) + this._instantiationService.createInstance(ExecutionStateCellStatusBarHelper, vm, newCell), + this._instantiationService.createInstance(TimerCellStatusBarHelper, vm, newCell), + this._instantiationService.createInstance(KeybindingPlaceholderStatusBarHelper, vm, newCell), ]; this._visibleCells.set(newCell.handle, helpers); } @@ -83,7 +89,7 @@ class ExecutionStateCellStatusBarHelper extends Disposable { super(); this._update(); - this._register(this._cell.model.onDidChangeMetadata(() => this._update())); + this._register(this._cell.model.onDidChangeInternalMetadata(() => this._update())); } private async _update() { @@ -101,13 +107,13 @@ class ExecutionStateCellStatusBarHelper extends Disposable { return undefined; // {{SQL CARBON EDIT}} Strict nulls } - const item = this._getItemForState(cell.metadata?.runState, cell.metadata?.lastRunSuccess); + const item = this._getItemForState(cell.internalMetadata.runState, cell.internalMetadata.lastRunSuccess); // Show the execution spinner for a minimum time - if (cell.metadata?.runState === NotebookCellExecutionState.Executing) { + if (cell.internalMetadata.runState === NotebookCellExecutionState.Executing) { this._currentExecutingStateTimer = setTimeout(() => { this._currentExecutingStateTimer = undefined; - if (cell.metadata?.runState !== NotebookCellExecutionState.Executing) { + if (cell.internalMetadata.runState !== NotebookCellExecutionState.Executing) { this._update(); } }, ExecutionStateCellStatusBarHelper.MIN_SPINNER_TIME); @@ -117,7 +123,7 @@ class ExecutionStateCellStatusBarHelper extends Disposable { } private _getItemForState(runState: NotebookCellExecutionState | undefined, lastRunSuccess: boolean | undefined): INotebookCellStatusBarItem | undefined { - if (runState === NotebookCellExecutionState.Idle && lastRunSuccess) { + if (!runState && lastRunSuccess) { return { text: '$(notebook-state-success)', color: themeColorFromId(cellStatusIconSuccess), @@ -125,7 +131,7 @@ class ExecutionStateCellStatusBarHelper extends Disposable { alignment: CellStatusbarAlignment.Left, priority: Number.MAX_SAFE_INTEGER }; - } else if (runState === NotebookCellExecutionState.Idle && lastRunSuccess === false) { + } else if (!runState && lastRunSuccess === false) { return { text: '$(notebook-state-error)', color: themeColorFromId(cellStatusIconError), @@ -173,23 +179,22 @@ class TimerCellStatusBarHelper extends Disposable { this._scheduler = this._register(new RunOnceScheduler(() => this._update(), TimerCellStatusBarHelper.UPDATE_INTERVAL)); this._update(); - this._register( - Event.filter(this._cell.model.onDidChangeMetadata, e => !!e.runStateChanged) - (() => this._update())); + this._register(this._cell.model.onDidChangeInternalMetadata(() => this._update())); } private async _update() { let item: INotebookCellStatusBarItem | undefined; - if (this._cell.metadata?.runState === NotebookCellExecutionState.Executing) { - const startTime = this._cell.metadata.runStartTime; - const adjustment = this._cell.metadata.runStartTimeAdjustment; + const state = this._cell.internalMetadata.runState; + if (state === NotebookCellExecutionState.Executing) { + const startTime = this._cell.internalMetadata.runStartTime; + const adjustment = this._cell.internalMetadata.runStartTimeAdjustment; if (typeof startTime === 'number') { item = this._getTimeItem(startTime, Date.now(), adjustment); this._scheduler.schedule(); } - } else if (this._cell.metadata?.runState === NotebookCellExecutionState.Idle) { - const startTime = this._cell.metadata.runStartTime; - const endTime = this._cell.metadata.runEndTime; + } else if (!state) { + const startTime = this._cell.internalMetadata.runStartTime; + const endTime = this._cell.internalMetadata.runEndTime; if (typeof startTime === 'number' && typeof endTime === 'number') { item = this._getTimeItem(startTime, endTime); } @@ -222,4 +227,80 @@ class TimerCellStatusBarHelper extends Disposable { } } +/** + * Shows a keybinding hint for the execute command + */ +class KeybindingPlaceholderStatusBarHelper extends Disposable { + private _currentItemIds: string[] = []; + private readonly _contextKeyService: IContextKeyService; + + constructor( + private readonly _notebookViewModel: NotebookViewModel, + private readonly _cell: ICellViewModel, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IContextKeyService _contextKeyService: IContextKeyService, + ) { + super(); + + // Create a fake ContextKeyService, and look up the keybindings within this context. + this._contextKeyService = this._register(_contextKeyService.createScoped(document.createElement('div'))); + InputFocusedContext.bindTo(this._contextKeyService).set(true); + EditorContextKeys.editorTextFocus.bindTo(this._contextKeyService).set(true); + EditorContextKeys.focus.bindTo(this._contextKeyService).set(true); + EditorContextKeys.textInputFocus.bindTo(this._contextKeyService).set(true); + NOTEBOOK_CELL_EXECUTION_STATE.bindTo(this._contextKeyService).set('idle'); + + this._update(); + this._register(this._cell.model.onDidChangeInternalMetadata(() => this._update())); + } + + private async _update() { + const items = this._getItemsForCell(this._cell); + if (Array.isArray(items)) { + this._currentItemIds = this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items }]); + } + } + + private _getItemsForCell(cell: ICellViewModel): INotebookCellStatusBarItem[] { + if (typeof cell.internalMetadata.runState !== 'undefined' || typeof cell.internalMetadata.lastRunSuccess !== 'undefined') { + return []; + } + + let text: string; + if (cell.cellKind === CellKind.Code) { + const keybinding = this._keybindingService.lookupKeybinding(EXECUTE_CELL_COMMAND_ID, this._contextKeyService)?.getLabel(); + if (!keybinding) { + return []; + } + + text = localize('notebook.cell.status.codeExecuteTip', "Press {0} to execute cell", keybinding); + } else { + const keybinding = this._keybindingService.lookupKeybinding(QUIT_EDIT_CELL_COMMAND_ID, this._contextKeyService)?.getLabel(); + if (!keybinding) { + return []; + } + + text = localize('notebook.cell.status.markdownExecuteTip', "Press {0} to stop editing", keybinding); + } + + const item = { + text, + tooltip: text, + alignment: CellStatusbarAlignment.Left, + opacity: '0.7', + onlyShowWhenActive: true, + priority: 100 + }; + + return [item]; + } + + + override dispose() { + super.dispose(); + + this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items: [] }]); + } +} + registerNotebookContribution(NotebookStatusBarController.id, NotebookStatusBarController); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/notebookVisibleCellObserver.ts b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/notebookVisibleCellObserver.ts index c0827f60e7..63b4440ed5 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/notebookVisibleCellObserver.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/notebookVisibleCellObserver.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { diffSets } from 'vs/base/common/collections'; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/statusBarProviders.ts b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/statusBarProviders.ts index 438e5cda4b..7f30918c31 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/statusBarProviders.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/statusBarProviders.ts @@ -1,75 +1,25 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { CHANGE_CELL_LANGUAGE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; import { CellKind, CellStatusbarAlignment, INotebookCellStatusBarItem, INotebookCellStatusBarItemList, INotebookCellStatusBarItemProvider } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { CHANGE_CELL_LANGUAGE, EXECUTE_CELL_COMMAND_ID, QUIT_EDIT_CELL_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { NotebookSelector } from 'vs/workbench/contrib/notebook/common/notebookSelector'; - -class CellStatusBarPlaceholderProvider implements INotebookCellStatusBarItemProvider { - readonly selector: NotebookSelector = { - pattern: '**/*' - }; - - constructor( - @INotebookService private readonly _notebookService: INotebookService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, - ) { } - - async provideCellStatusBarItems(uri: URI, index: number, token: CancellationToken): Promise { - const doc = this._notebookService.getNotebookTextModel(uri); - const cell = doc?.cells[index]; - if (!cell || typeof cell.metadata.runState !== 'undefined' || typeof cell.metadata.lastRunSuccess !== 'undefined') { - return undefined; // {{SQL CARBON EDIT}} Strict nulls - } - - let text: string; - if (cell.cellKind === CellKind.Code) { - const keybinding = this._keybindingService.lookupKeybinding(EXECUTE_CELL_COMMAND_ID)?.getLabel(); - if (!keybinding) { - return undefined; // {{SQL CARBON EDIT}} Strict nulls - } - - text = localize('notebook.cell.status.codeExecuteTip', "Press {0} to execute cell", keybinding); - } else { - const keybinding = this._keybindingService.lookupKeybinding(QUIT_EDIT_CELL_COMMAND_ID)?.getLabel(); - if (!keybinding) { - return undefined; // {{SQL CARBON EDIT}} Strict nulls - } - - text = localize('notebook.cell.status.markdownExecuteTip', "Press {0} to stop editing", keybinding); - } - - const item = { - text, - tooltip: text, - alignment: CellStatusbarAlignment.Left, - opacity: '0.7', - onlyShowWhenActive: true - }; - return { - items: [item] - }; - } -} class CellStatusBarLanguagePickerProvider implements INotebookCellStatusBarItemProvider { - readonly selector: NotebookSelector = { - pattern: '**/*' - }; + + readonly viewType = '*'; constructor( @INotebookService private readonly _notebookService: INotebookService, @@ -83,7 +33,7 @@ class CellStatusBarLanguagePickerProvider implements INotebookCellStatusBarItemP return undefined; // {{SQL CARBON EDIT}} Strict nulls } - const modeId = cell.cellKind === CellKind.Markdown ? + const modeId = cell.cellKind === CellKind.Markup ? 'markdown' : (this._modeService.getModeIdForLanguageName(cell.language) || cell.language); const text = this._modeService.getLanguageName(modeId) || this._modeService.getLanguageName('plaintext'); @@ -107,7 +57,6 @@ class BuiltinCellStatusBarProviders extends Disposable { super(); const builtinProviders = [ - CellStatusBarPlaceholderProvider, CellStatusBarLanguagePickerProvider, ]; builtinProviders.forEach(p => { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts b/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts index b2e8c57a96..c21b63776d 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/undoRedo/notebookUndoRedo.ts b/src/vs/workbench/contrib/notebook/browser/contrib/undoRedo/notebookUndoRedo.ts index b198ae8de1..d430d932ff 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/undoRedo/notebookUndoRedo.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/undoRedo/notebookUndoRedo.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; @@ -9,7 +9,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { CellEditState, getNotebookEditorFromEditorPane, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { RedoCommand, UndoCommand } from 'vs/editor/browser/editorExtensions'; class NotebookUndoRedoContribution extends Disposable { @@ -24,12 +24,12 @@ class NotebookUndoRedoContribution extends Disposable { return editor.viewModel.undo().then(cellResources => { if (cellResources?.length) { editor?.viewModel?.viewCells.forEach(cell => { - if (cell.cellKind === CellKind.Markdown && cellResources.find(resource => resource.fragment === cell.model.uri.fragment)) { + if (cell.cellKind === CellKind.Markup && cellResources.find(resource => resource.fragment === cell.model.uri.fragment)) { cell.updateEditState(CellEditState.Editing, 'undo'); } }); - editor?.setOptions(new NotebookEditorOptions({ cellOptions: { resource: cellResources[0] }, preserveFocus: true })); + editor?.setOptions({ cellOptions: { resource: cellResources[0] }, preserveFocus: true }); } }); } @@ -43,12 +43,12 @@ class NotebookUndoRedoContribution extends Disposable { return editor.viewModel.redo().then(cellResources => { if (cellResources?.length) { editor?.viewModel?.viewCells.forEach(cell => { - if (cell.cellKind === CellKind.Markdown && cellResources.find(resource => resource.fragment === cell.model.uri.fragment)) { + if (cell.cellKind === CellKind.Markup && cellResources.find(resource => resource.fragment === cell.model.uri.fragment)) { cell.updateEditState(CellEditState.Editing, 'redo'); } }); - editor?.setOptions(new NotebookEditorOptions({ cellOptions: { resource: cellResources[0] }, preserveFocus: true })); + editor?.setOptions({ cellOptions: { resource: cellResources[0] }, preserveFocus: true }); } }); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/undoRedo/test/notebookUndoRedo.test.ts b/src/vs/workbench/contrib/notebook/browser/contrib/undoRedo/test/notebookUndoRedo.test.ts index 5c4358699b..77f3053ea1 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/undoRedo/test/notebookUndoRedo.test.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/undoRedo/test/notebookUndoRedo.test.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; @@ -12,8 +12,8 @@ suite('Notebook Undo/Redo', () => { test('Basics', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], ], async (editor, accessor) => { const modeService = accessor.get(IModeService); @@ -57,8 +57,8 @@ suite('Notebook Undo/Redo', () => { test('Invalid replace count should not throw', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], ], async (editor, accessor) => { const modeService = accessor.get(IModeService); @@ -81,8 +81,8 @@ suite('Notebook Undo/Redo', () => { test('Replace beyond length', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], ], async (editor) => { const viewModel = editor.viewModel; @@ -100,8 +100,8 @@ suite('Notebook Undo/Redo', () => { test('Invalid replace count should not affect undo/redo', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], ], async (editor, accessor) => { const modeService = accessor.get(IModeService); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/viewportCustomMarkdown/viewportCustomMarkdown.ts b/src/vs/workbench/contrib/notebook/browser/contrib/viewportCustomMarkdown/viewportCustomMarkdown.ts index ac8c52e30f..c66d36918f 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/viewportCustomMarkdown/viewportCustomMarkdown.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/viewportCustomMarkdown/viewportCustomMarkdown.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; @@ -13,7 +13,7 @@ import { BUILTIN_RENDERER_ID, CellKind } from 'vs/workbench/contrib/notebook/com import { cellRangesToIndexes } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -class NotebookClipboardContribution extends Disposable implements INotebookEditorContribution { +class NotebookViewportContribution extends Disposable implements INotebookEditorContribution { static id: string = 'workbench.notebook.viewportCustomMarkdown'; private readonly _warmupViewport: RunOnceScheduler; @@ -22,18 +22,26 @@ class NotebookClipboardContribution extends Disposable implements INotebookEdito super(); this._warmupViewport = new RunOnceScheduler(() => this._warmupViewportNow(), 200); - + this._register(this._warmupViewport); this._register(this._notebookEditor.onDidScroll(() => { this._warmupViewport.schedule(); })); } private _warmupViewportNow() { + if (this._notebookEditor.isDisposed) { + return; + } + + if (!this._notebookEditor.hasModel()) { + return; + } + const visibleRanges = this._notebookEditor.getVisibleRangesPlusViewportAboveBelow(); cellRangesToIndexes(visibleRanges).forEach(index => { const cell = this._notebookEditor.viewModel?.viewCells[index]; - if (cell?.cellKind === CellKind.Markdown && cell?.getEditState() === CellEditState.Preview && !cell.metadata?.inputCollapsed) { + if (cell?.cellKind === CellKind.Markup && cell?.getEditState() === CellEditState.Preview && !cell.metadata.inputCollapsed) { this._notebookEditor.createMarkdownPreview(cell); } else if (cell?.cellKind === CellKind.Code) { const viewCell = (cell as CodeCellViewModel); @@ -50,10 +58,14 @@ class NotebookClipboardContribution extends Disposable implements INotebookEdito return; } + if (!this._notebookEditor.hasModel()) { + return; + } + if (pickedMimeTypeRenderer.rendererId === BUILTIN_RENDERER_ID) { const renderer = this._notebookEditor.getOutputRenderer().getContribution(pickedMimeTypeRenderer.mimeType); if (renderer?.getType() === RenderOutputType.Html) { - const renderResult = renderer!.render(output, output.model.outputs.filter(op => op.mime === pickedMimeTypeRenderer.mimeType), DOM.$(''), undefined) as IInsetRenderOutput; + const renderResult = renderer.render(output, output.model.outputs.filter(op => op.mime === pickedMimeTypeRenderer.mimeType)[0], DOM.$(''), this._notebookEditor.viewModel.uri) as IInsetRenderOutput; this._notebookEditor.createOutput(viewCell, renderResult, 0); } return; @@ -72,4 +84,4 @@ class NotebookClipboardContribution extends Disposable implements INotebookEdito } } -registerNotebookContribution(NotebookClipboardContribution.id, NotebookClipboardContribution); +registerNotebookContribution(NotebookViewportContribution.id, NotebookViewportContribution); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index ba44758a45..f348c2f83a 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -5,11 +5,11 @@ import * as DOM from 'vs/base/browser/dom'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; 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'; @@ -25,7 +25,6 @@ import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/men 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'; @@ -41,11 +40,12 @@ import * as editorCommon from 'vs/editor/common/editorCommon'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +const fixedEditorPadding = { + top: 12, + bottom: 12 +}; export const fixedEditorOptions: IEditorOptions = { - padding: { - top: 12, - bottom: 12 - }, + padding: fixedEditorPadding, scrollBeyondLastLine: false, scrollbar: { verticalScrollbarSize: 14, @@ -234,7 +234,7 @@ class PropertyHeader extends Disposable { } } - private _updateFoldingIcon(): any { + private _updateFoldingIcon() { if (this.accessor.getFoldingState(this.cell) === PropertyFoldingState.Collapsed) { DOM.reset(this._foldingIndicator, renderIcon(collapsedIcon)); this._propertyExpanded?.set(false); @@ -491,22 +491,6 @@ abstract class AbstractElementRenderer extends Disposable { } break; - case 'executionOrder': - // 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; default: result[key] = newMetadataObj[key]; break; @@ -550,8 +534,8 @@ abstract class AbstractElementRenderer extends Disposable { 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)); + const originalMetadataModel = await this.textModelService.createModelReference(CellUri.generateCellUri(this.cell.originalDocument.uri, this.cell.original!.handle, Schemas.vscodeNotebookCellMetadata)); + const modifiedMetadataModel = await this.textModelService.createModelReference(CellUri.generateCellUri(this.cell.modifiedDocument.uri, this.cell.modified!.handle, Schemas.vscodeNotebookCellMetadata)); this._metadataEditor.setModel({ original: originalMetadataModel.object.textEditorModel, modified: modifiedMetadataModel.object.textEditorModel @@ -613,7 +597,7 @@ abstract class AbstractElementRenderer extends Disposable { ? this.cell.modified!.handle : this.cell.original!.handle; - const modelUri = CellUri.generateCellMetadataUri(uri, handle); + const modelUri = CellUri.generateCellUri(uri, handle, Schemas.vscodeNotebookCellMetadata); const metadataModel = this.modelService.createModel(originalMetadataSource, mode, modelUri, false); this._metadataEditor.setModel(metadataModel); this._metadataEditorDisposeStore.add(metadataModel); @@ -979,7 +963,7 @@ export class DeletedElement extends SingleSideDiffElement { 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; + const editorHeight = lineCount * lineHeight + fixedEditorPadding.top + fixedEditorPadding.bottom; this._editor = this.templateData.sourceEditor; this._editor.layout({ @@ -1130,7 +1114,7 @@ export class InsertElement extends SingleSideDiffElement { 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; + const editorHeight = lineCount * lineHeight + fixedEditorPadding.top + fixedEditorPadding.bottom; this._editor = this.templateData.sourceEditor; this._editor.layout( @@ -1468,7 +1452,8 @@ export class ModifiedElement extends AbstractElementRenderer { 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; + + const editorHeight = this.cell.layoutInfo.editorHeight !== 0 ? this.cell.layoutInfo.editorHeight : lineCount * lineHeight + fixedEditorPadding.top + fixedEditorPadding.bottom; this._editorContainer = this.templateData.editorContainer; this._editor = this.templateData.sourceEditor; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts index 213743dc06..3940701380 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts @@ -14,7 +14,6 @@ import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/r 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'; @@ -90,7 +89,7 @@ export class OutputElement extends Disposable { result = this._notebookEditor.getOutputRenderer().render(this.output, innerContainer, pickedMimeTypeRenderer.mimeType, this._notebookTextModel.uri); } - this.output.pickedMimeType = pick; + this.output.pickedMimeType = pickedMimeTypeRenderer; } this.domNode = outputItemDiv; @@ -202,7 +201,7 @@ export class OutputElement extends Disposable { ); } - viewModel.pickedMimeType = pick; + viewModel.pickedMimeType = mimeTypes[pick]; this.render(index, nextElement as HTMLElement); } } @@ -250,9 +249,7 @@ export class OutputContainer extends Disposable { private _outputContainer: HTMLElement, @INotebookService private _notebookService: INotebookService, @IQuickInputService private readonly _quickInputService: IQuickInputService, - @IOpenerService readonly _openerService: IOpenerService, - @ITextFileService readonly _textFileService: ITextFileService, - + @IOpenerService readonly _openerService: IOpenerService ) { super(); this._register(this._diffElementViewModel.onDidLayoutChange(() => { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts index 793094ce00..6d7855318a 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts @@ -45,15 +45,17 @@ registerAction2(class extends Action2 { const activeEditor = editorService.activeEditorPane; if (activeEditor && activeEditor instanceof NotebookTextDiffEditor) { const diffEditorInput = activeEditor.input as NotebookDiffEditorInput; - const leftResource = diffEditorInput.originalResource; - const rightResource = diffEditorInput.resource; - const options = { - preserveFocus: false - }; - const label = diffEditorInput.textDiffName; - const input = editorService.createEditorInput({ leftResource, rightResource, label, options }); - await editorService.openEditor(input, { override: EditorOverride.DISABLED }, viewColumnToEditorGroup(editorGroupService, undefined)); + await editorService.openEditor( + { + originalInput: { resource: diffEditorInput.originalResource }, + modifiedInput: { resource: diffEditorInput.resource }, + label: diffEditorInput.textDiffName, + options: { + preserveFocus: false, + override: EditorOverride.DISABLED + } + }, viewColumnToEditorGroup(editorGroupService, undefined)); } } }); @@ -190,14 +192,14 @@ registerAction2(class extends Action2 { } run(accessor: ServicesAccessor, context?: { cell: DiffElementViewModelBase }) { if (!context) { - return undefined; + return undefined; // {{SQL CARBON EDIT}} Strict nulls } const original = context.cell.original; const modified = context.cell.modified; if (!original || !modified) { - return undefined; + return undefined; // {{SQL CARBON EDIT}} Strict nulls } const bulkEditService = accessor.get(IBulkEditService); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts index 1467cd763e..e91460558b 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts @@ -15,6 +15,7 @@ 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'; +import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; export enum DiffSide { Original = 0, @@ -26,6 +27,7 @@ export interface IDiffCellInfo extends ICommonCellInfo { } export interface INotebookTextDiffEditor extends ICommonNotebookEditor { + notebookOptions: NotebookOptions; readonly textModel?: NotebookTextModel; onMouseUp: Event<{ readonly event: MouseEvent; readonly target: DiffElementViewModelBase; }>; onDidDynamicOutputRendered: Event<{ cell: IGenericCellViewModel, output: ICellOutputViewModel }>; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts index d0167f7060..3bd9875583 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts @@ -8,7 +8,7 @@ import * as DOM from 'vs/base/browser/dom'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { notebookCellBorder, NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { NotebookDiffEditorInput } from '../notebookDiffEditorInput'; @@ -20,10 +20,10 @@ 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 { IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BareFontInfo, FontInfo } from 'vs/editor/common/config/fontInfo'; import { getPixelRatio, getZoomLevel } from 'vs/base/browser/browser'; -import { CellEditState, ICellOutputViewModel, IDisplayOutputLayoutUpdateRequest, IGenericCellViewModel, IInsetRenderOutput, NotebookLayoutInfo, NOTEBOOK_DIFF_EDITOR_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, ICellOutputViewModel, IDisplayOutputLayoutUpdateRequest, IGenericCellViewModel, IInsetRenderOutput, INotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_DIFF_EDITOR_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { DiffSide, DIFF_CELL_MARGIN, IDiffCellInfo, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -38,9 +38,9 @@ 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, MARKDOWN_PREVIEW_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; import { NotebookDiffEditorEventDispatcher, NotebookDiffLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher'; import { readFontInfo } from 'vs/editor/browser/config/configuration'; +import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; const $ = DOM.$; @@ -72,9 +72,15 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD private _revealFirst: boolean; private readonly _insetModifyQueueByOutputId = new SequencerByKey(); - protected _onDidDynamicOutputRendered = new Emitter<{ cell: IGenericCellViewModel, output: ICellOutputViewModel }>(); + protected _onDidDynamicOutputRendered = new Emitter<{ cell: IGenericCellViewModel, output: ICellOutputViewModel; }>(); onDidDynamicOutputRendered = this._onDidDynamicOutputRendered.event; + private _notebookOptions: NotebookOptions; + + get notebookOptions() { + return this._notebookOptions; + } + private readonly _localStore = this._register(new DisposableStore()); private _isDisposed: boolean = false; @@ -93,10 +99,11 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD @IStorageService storageService: IStorageService, ) { super(NotebookTextDiffEditor.ID, telemetryService, themeService, storageService); - const editorOptions = this.configurationService.getValue('editor'); + this._notebookOptions = new NotebookOptions(this.configurationService); + this._register(this._notebookOptions); + const editorOptions = this.configurationService.getValue('editor'); this._fontInfo = readFontInfo(BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel(), getPixelRatio())); this._revealFirst = true; - this._outputRenderer = new OutputRenderer(this, this.instantiationService); } @@ -136,10 +143,10 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD setMarkdownCellEditState(cellId: string, editState: CellEditState): void { // throw new Error('Method not implemented.'); } - markdownCellDragStart(cellId: string, position: { clientY: number }): void { + markdownCellDragStart(cellId: string, event: { dragOffsetY: number; }): void { // throw new Error('Method not implemented.'); } - markdownCellDrag(cellId: string, position: { clientY: number }): void { + markdownCellDrag(cellId: string, event: { dragOffsetY: number; }): void { // throw new Error('Method not implemented.'); } markdownCellDragEnd(cellId: string): void { @@ -285,7 +292,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } } - override async setInput(input: NotebookDiffEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override async setInput(input: NotebookDiffEditorInput, options: INotebookEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { await super.setInput(input, options, context, token); const model = await input.resolve(); @@ -366,21 +373,12 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD })); } - private readonly webviewOptions = { - outputNodePadding: CELL_OUTPUT_PADDING, - outputNodeLeftPadding: 32, - previewNodePadding: MARKDOWN_PREVIEW_PADDING, - leftMargin: 0, - rightMargin: 0, - runGutter: 0 - }; - private async _createModifiedWebview(id: string, resource: URI): Promise { if (this._modifiedWebview) { this._modifiedWebview.dispose(); } - this._modifiedWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, this.webviewOptions) as BackLayerWebView; + this._modifiedWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, this._notebookOptions.computeDiffWebviewOptions(), undefined) as BackLayerWebView; // attach the webview container to the DOM tree first this._list.rowsContainer.insertAdjacentElement('afterbegin', this._modifiedWebview.element); await this._modifiedWebview.createWebview(); @@ -393,7 +391,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._originalWebview.dispose(); } - this._originalWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, this.webviewOptions) as BackLayerWebView; + this._originalWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, this._notebookOptions.computeDiffWebviewOptions(), undefined) as BackLayerWebView; // attach the webview container to the DOM tree first this._list.rowsContainer.insertAdjacentElement('afterbegin', this._originalWebview.element); await this._originalWebview.createWebview(); @@ -413,16 +411,43 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._originalWebview?.removeInsets([...this._originalWebview?.insetMapping.keys()]); this._modifiedWebview?.removeInsets([...this._modifiedWebview?.insetMapping.keys()]); - this._diffElementViewModels = viewModels; - this._list.splice(0, this._list.length, this._diffElementViewModels); + this._setViewModel(viewModels); + // this._diffElementViewModels = viewModels; + // this._list.splice(0, this._list.length, this._diffElementViewModels); - if (this._revealFirst && firstChangeIndex !== -1) { + if (this._revealFirst && firstChangeIndex !== -1 && firstChangeIndex < this._list.length) { this._revealFirst = false; this._list.setFocus([firstChangeIndex]); this._list.reveal(firstChangeIndex, 0.3); } } + private _setViewModel(viewModels: DiffElementViewModelBase[]) { + let isSame = true; + if (this._diffElementViewModels.length === viewModels.length) { + for (let i = 0; i < viewModels.length; i++) { + const a = this._diffElementViewModels[i]; + const b = viewModels[i]; + + if (a.original?.textModel.getHashValue() !== b.original?.textModel.getHashValue() + || a.modified?.textModel.getHashValue() !== b.modified?.textModel.getHashValue()) { + isSame = false; + break; + } + } + } else { + isSame = false; + } + + if (isSame) { + return; + } + + this._diffElementViewModels = viewModels; + this._list.splice(0, this._list.length, this._diffElementViewModels); + + } + /** * making sure that swapping cells are always translated to `insert+delete`. */ @@ -741,6 +766,8 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._modifiedResourceDisposableStore.clear(); this._list?.splice(0, this._list?.length || 0); + this._model = null; + this._diffElementViewModels = []; } getOutputRenderer(): OutputRenderer { diff --git a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts index fa775f79cf..aeda144a65 100644 --- a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts +++ b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts @@ -6,73 +6,58 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as nls from 'vs/nls'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { NotebookEditorPriority } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookEditorPriority, NotebookRendererEntrypoint, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon'; namespace NotebookEditorContribution { - export const viewType = 'viewType'; + export const type = 'type'; export const displayName = 'displayName'; export const selector = 'selector'; export const priority = 'priority'; } export interface INotebookEditorContribution { - readonly [NotebookEditorContribution.viewType]: string; + readonly [NotebookEditorContribution.type]: string; readonly [NotebookEditorContribution.displayName]: string; readonly [NotebookEditorContribution.selector]?: readonly { filenamePattern?: string; excludeFileNamePattern?: string; }[]; readonly [NotebookEditorContribution.priority]?: string; } namespace NotebookRendererContribution { - export const viewType = 'viewType'; + export const id = 'id'; export const displayName = 'displayName'; export const mimeTypes = 'mimeTypes'; export const entrypoint = 'entrypoint'; export const hardDependencies = 'dependencies'; export const optionalDependencies = 'optionalDependencies'; + export const requiresMessaging = 'requiresMessaging'; } export interface INotebookRendererContribution { readonly [NotebookRendererContribution.id]?: string; - readonly [NotebookRendererContribution.viewType]?: string; readonly [NotebookRendererContribution.displayName]: string; readonly [NotebookRendererContribution.mimeTypes]?: readonly string[]; - readonly [NotebookRendererContribution.entrypoint]: string; + readonly [NotebookRendererContribution.entrypoint]: NotebookRendererEntrypoint; readonly [NotebookRendererContribution.hardDependencies]: readonly string[]; readonly [NotebookRendererContribution.optionalDependencies]: readonly string[]; -} - -enum NotebookMarkupRendererContribution { - id = 'id', - displayName = 'displayName', - entrypoint = 'entrypoint', - dependsOn = 'dependsOn', - mimeTypes = 'mimeTypes', -} - -export interface INotebookMarkupRendererContribution { - readonly [NotebookMarkupRendererContribution.id]?: string; - readonly [NotebookMarkupRendererContribution.displayName]: string; - readonly [NotebookMarkupRendererContribution.entrypoint]: string; - readonly [NotebookMarkupRendererContribution.dependsOn]: string | undefined; - readonly [NotebookMarkupRendererContribution.mimeTypes]: string[] | undefined; + readonly [NotebookRendererContribution.requiresMessaging]: RendererMessagingSpec; } const notebookProviderContribution: IJSONSchema = { description: nls.localize('contributes.notebook.provider', 'Contributes notebook document provider.'), type: 'array', - defaultSnippets: [{ body: [{ viewType: '', displayName: '' }] }], + defaultSnippets: [{ body: [{ type: '', displayName: '', 'selector': [{ 'filenamePattern': '' }] }] }], items: { type: 'object', required: [ - NotebookEditorContribution.viewType, + NotebookEditorContribution.type, NotebookEditorContribution.displayName, NotebookEditorContribution.selector, ], properties: { - [NotebookEditorContribution.viewType]: { + [NotebookEditorContribution.type]: { type: 'string', - description: nls.localize('contributes.notebook.provider.viewType', 'Unique identifier of the notebook.'), + description: nls.localize('contributes.notebook.provider.viewType', 'Type of the notebook.'), }, [NotebookEditorContribution.displayName]: { type: 'string', @@ -129,11 +114,6 @@ const notebookRendererContribution: IJSONSchema = { type: 'string', description: nls.localize('contributes.notebook.renderer.viewType', 'Unique identifier of the notebook output renderer.'), }, - [NotebookRendererContribution.viewType]: { - type: 'string', - deprecationMessage: nls.localize('contributes.notebook.provider.viewType.deprecated', 'Rename `viewType` to `id`.'), - description: nls.localize('contributes.notebook.renderer.viewType', 'Unique identifier of the notebook output renderer.'), - }, [NotebookRendererContribution.displayName]: { type: 'string', description: nls.localize('contributes.notebook.renderer.displayName', 'Human readable name of the notebook output renderer.'), @@ -146,8 +126,27 @@ const notebookRendererContribution: IJSONSchema = { } }, [NotebookRendererContribution.entrypoint]: { - type: 'string', description: nls.localize('contributes.notebook.renderer.entrypoint', 'File to load in the webview to render the extension.'), + oneOf: [ + { + type: 'string', + }, + // todo@connor4312 + @mjbvz: uncomment this once it's ready for external adoption + // { + // type: 'object', + // required: ['extends', 'path'], + // properties: { + // extends: { + // type: 'string', + // description: nls.localize('contributes.notebook.renderer.entrypoint.extends', 'Existing renderer that this one extends.'), + // }, + // path: { + // type: 'string', + // description: nls.localize('contributes.notebook.renderer.entrypoint', 'File to load in the webview to render the extension.'), + // }, + // } + // } + ] }, [NotebookRendererContribution.hardDependencies]: { type: 'array', @@ -161,60 +160,33 @@ const notebookRendererContribution: IJSONSchema = { items: { type: 'string' }, markdownDescription: nls.localize('contributes.notebook.renderer.optionalDependencies', 'List of soft kernel dependencies the renderer can make use of. If any of the dependencies are present in the `NotebookKernel.preloads`, the renderer will be preferred over renderers that don\'t interact with the kernel.'), }, - } - } -}; -const notebookMarkupRendererContribution: IJSONSchema = { - description: nls.localize('contributes.notebook.markdownRenderer', 'Contributes a renderer for markdown cells in notebooks.'), - type: 'array', - defaultSnippets: [{ body: [{ id: '', displayName: '', entrypoint: '' }] }], - items: { - type: 'object', - required: [ - NotebookMarkupRendererContribution.id, - NotebookMarkupRendererContribution.displayName, - NotebookMarkupRendererContribution.entrypoint, - ], - properties: { - [NotebookMarkupRendererContribution.id]: { - type: 'string', - description: nls.localize('contributes.notebook.markdownRenderer.id', 'Unique identifier of the notebook markdown renderer.'), - }, - [NotebookMarkupRendererContribution.displayName]: { - type: 'string', - description: nls.localize('contributes.notebook.markdownRenderer.displayName', 'Human readable name of the notebook markdown renderer.'), - }, - [NotebookMarkupRendererContribution.entrypoint]: { - type: 'string', - description: nls.localize('contributes.notebook.markdownRenderer.entrypoint', 'File to load in the webview to render the extension.'), - }, - [NotebookMarkupRendererContribution.mimeTypes]: { - type: 'array', - items: { type: 'string' }, - description: nls.localize('contributes.notebook.markdownRenderer.mimeTypes', 'The mime type that the renderer handles.'), - }, - [NotebookMarkupRendererContribution.dependsOn]: { - type: 'string', - description: nls.localize('contributes.notebook.markdownRenderer.dependsOn', 'If specified, this renderer augments another renderer instead of providing full rendering.'), + [NotebookRendererContribution.requiresMessaging]: { + default: 'never', + enum: [ + 'always', + 'optional', + 'never', + ], + + enumDescriptions: [ + nls.localize('contributes.notebook.renderer.requiresMessaging.always', 'Messaging is required. The renderer will only be used when it\'s part of an extension that can be run in an extension host.'), + nls.localize('contributes.notebook.renderer.requiresMessaging.optional', 'The renderer is better with messaging available, but it\'s not requried.'), + nls.localize('contributes.notebook.renderer.requiresMessaging.never', 'The renderer does not require messaging.'), + ], + description: nls.localize('contributes.notebook.renderer.requiresMessaging', 'Defines how and if the renderer needs to communicate with an extension host, via `createRendererMessaging`. Renderers with stronger messaging requirements may not work in all environments.'), }, } } }; -export const notebookProviderExtensionPoint = ExtensionsRegistry.registerExtensionPoint( +export const notebooksExtensionPoint = ExtensionsRegistry.registerExtensionPoint( { - extensionPoint: 'notebookProvider', + extensionPoint: 'notebooks', jsonSchema: notebookProviderContribution }); export const notebookRendererExtensionPoint = ExtensionsRegistry.registerExtensionPoint( { - extensionPoint: 'notebookOutputRenderer', + extensionPoint: 'notebookRenderer', jsonSchema: notebookRendererContribution }); - -export const notebookMarkupRendererExtensionPoint = ExtensionsRegistry.registerExtensionPoint( - { - extensionPoint: 'notebookMarkupRenderers', - jsonSchema: notebookMarkupRendererContribution - }); diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 9c343bbe3b..dc20f45f0a 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -11,23 +11,81 @@ position: relative; } -.monaco-workbench .notebookOverlay .notebook-top-toolbar { +.monaco-workbench .notebookOverlay .notebook-toolbar-container { width: 100%; - display: inline-flex; - padding-left: 8px; - margin-top: 4px; + display: none; + margin-top: 2px; + margin-bottom: 2px; } -.monaco-workbench .notebookOverlay .notebook-top-toolbar .monaco-action-bar .action-item { - width: 24px; +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item { height: 22px; display: flex; align-items: center; + border-radius: 5px; + margin-right: 8px; } -.monaco-workbench .notebookOverlay .notebook-top-toolbar .monaco-action-bar .action-item .action-label { +.monaco-workbench .notebookOverlay .notebook-toolbar-container > .monaco-scrollable-element { + flex: 1; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container > .monaco-scrollable-element .notebook-toolbar-left { + padding: 0px 8px; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .notebook-toolbar-right { + display: flex; + padding: 0px 0px 0px 8px; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item .kernel-label { background-size: 16px; - margin: 4px 4px 0 4px; + padding: 0px 5px 0px 3px; + border-radius: 5px; + font-size: 13px; + height: 22px; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .notebook-toolbar-left .monaco-action-bar .action-item .action-label.separator { + margin: 5px 0px !important; + padding: 0px !important; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item:hover { + background-color: var(--code-toolbarHoverBackground); +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item .action-label { + background-size: 16px; + padding-left: 2px; +} + +.monaco-workbench .notebook-action-view-item .action-label { + display: inline-flex; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item .notebook-label { + background-size: 16px; + padding: 0px 5px 0px 2px; + border-radius: 5px; + background-color: unset; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item.disabled .notebook-label { + opacity: 0.4; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar:not(.vertical) .action-item.active .action-label:not(.disabled) { + background-color: unset; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar:not(.vertical) .action-label:not(.disabled):hover { + background-color: unset; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar:not(.vertical) .action-item.active { + background-color: unset; } .monaco-workbench .cell.markdown { @@ -162,6 +220,7 @@ .monaco-workbench .notebookOverlay .output { position: absolute; height: 0px; + font-size: var(--notebook-cell-output-font-size); user-select: text; -webkit-user-select: text; -ms-user-select: text; @@ -200,18 +259,21 @@ color: red; /*TODO@rebornix theme color*/ } -.monaco-workbench .notebookOverlay .cell-drag-image .output .multi-mimetype-output { +.monaco-workbench .notebookOverlay .cell-drag-image .output .cell-output-toolbar { display: none; } -.monaco-workbench .notebookOverlay .output .multi-mimetype-output { +.monaco-workbench .notebookOverlay .output .cell-output-toolbar { position: absolute; top: 4px; - left: -30px; - width: 16px; + left: -32px; height: 16px; cursor: pointer; - padding: 6px; + padding: 6px 0px; +} + +.monaco-workbench .notebookOverlay .output .cell-output-toolbar .actions-container { + justify-content: center; } .monaco-workbench .notebookOverlay .output pre { @@ -293,44 +355,17 @@ display: none; } -/* top and bottom borders on cells */ -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before, -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom:before, -.monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row .cell-inner-container:before, -.monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row .cell-inner-container:after { - content: ""; - position: absolute; - width: 100%; - height: 1px; +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .notebook-folding-indicator.mouseover .codicon { + opacity: 0; + transition: opacity 0.s; } -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before, -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { - content: ""; - position: absolute; - width: 1px; - height: 100%; - z-index: 10; +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .markdown-cell-hover .notebook-folding-indicator.mouseover .codicon { + opacity: 1; } -/* top border */ -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before { - border-top: 1px solid transparent; -} - -/* left border */ -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before { - border-left: 1px solid transparent; -} - -/* bottom border */ -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom:before { - border-bottom: 1px solid transparent; -} - -/* right border */ -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { - border-right: 1px solid transparent; +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .notebook-folding-indicator.mouseover .codicon { + opacity: 1; } .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before { @@ -368,19 +403,7 @@ z-index: 50; } -.monaco-workbench .notebookOverlay.cell-title-toolbar-right > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { - right: 44px; -} - -.monaco-workbench .notebookOverlay.cell-title-toolbar-left > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { - left: 76px; -} - -.monaco-workbench .notebookOverlay.cell-title-toolbar-hidden > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { - display: none; -} - -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar .action-item { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar .action-item.menu-entry { width: 24px; height: 24px; display: flex; @@ -410,7 +433,7 @@ overflow: hidden; } -.monaco-workbench .notebookOverlay.cell-statusbar-hidden .cell-statusbar-container { +.monaco-workbench .notebookOverlay .cell-statusbar-hidden .cell-statusbar-container { display: none; } @@ -471,7 +494,7 @@ .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container { position: absolute; flex-shrink: 0; - z-index: 27; /* Above the drag handle */ + z-index: 29; /* Above the drag handle, output, and toolbars */ } .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container .monaco-toolbar { @@ -479,8 +502,7 @@ height: initial; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container .monaco-toolbar .codicon { - margin: 0 4px 0 0; +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container .monaco-toolbar .action-item:not(.monaco-dropdown-with-primary) .codicon { padding: 6px; } @@ -490,6 +512,7 @@ .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .run-button-container .monaco-toolbar, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .run-button-container .monaco-toolbar, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-run-toolbar-dropdown-active .run-button-container .monaco-toolbar, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .run-button-container .monaco-toolbar { visibility: visible; } @@ -503,12 +526,14 @@ opacity: .6; /* Sizing hacks */ - left: 26px; - width: 35px; bottom: 0px; text-align: center; } +.monaco-workbench .notebookOverlay>.cell-list-container>.monaco-list>.monaco-scrollable-element>.monaco-list-rows>.monaco-list-row .cell-statusbar-hidden .execution-count-label { + line-height: 15px; +} + .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .output-collapsed .execution-count-label { bottom: 24px; } @@ -530,16 +555,26 @@ height: 2px; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list:focus-within > .monaco-scrollable-element > .monaco-list-rows:not(:hover) > .monaco-list-row.focused .cell-has-toolbar-actions .cell-title-toolbar, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell-has-toolbar-actions .cell-title-toolbar, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .markdown-cell-hover.cell-has-toolbar-actions .cell-title-toolbar, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-has-toolbar-actions.cell-output-hover .cell-title-toolbar, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-has-toolbar-actions:hover .cell-title-toolbar, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar:hover, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-toolbar-dropdown-active .cell-title-toolbar { +/* toolbar visible on hover */ +.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list:focus-within > .monaco-scrollable-element > .monaco-list-rows:not(:hover) > .monaco-list-row.focused .cell-has-toolbar-actions .cell-title-toolbar, +.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell-has-toolbar-actions .cell-title-toolbar, +.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .markdown-cell-hover.cell-has-toolbar-actions .cell-title-toolbar, +.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-has-toolbar-actions.cell-output-hover .cell-title-toolbar, +.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-has-toolbar-actions:hover .cell-title-toolbar, +.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar:hover, +.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-toolbar-dropdown-active .cell-title-toolbar { opacity: 1; } +/* toolbar visible on click */ +.monaco-workbench .notebookOverlay.cell-toolbar-click > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { + visibility: hidden; +} +.monaco-workbench .notebookOverlay.cell-toolbar-click > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell-title-toolbar { + opacity: 1; + visibility: visible; +} + .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list:not(.element-focused):focus:before { outline: none !important; } @@ -574,6 +609,26 @@ right: 0px; } +/** cell border colors */ + +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-editor-focus .cell-focus-indicator-top:before, +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-editor-focus .cell-focus-indicator-bottom:before, +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container.cell-editor-focus:before { + border-color: var(notebook-selected-cell-border-color) !important; +} + +.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 { + border-color: var(--notebook-inactive-focused-cell-border-color) !important; +} + +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-top:before, +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-bottom:before, +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-left:before, +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-right:before { + border-color: var(--notebook-focused-cell-border-color) !important; +} + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-drag-handle { position: absolute; top: 0px; @@ -851,7 +906,7 @@ position: absolute; top: 10px; - left: 8px; + left: 6px; display: flex; justify-content: center; align-items: center; @@ -865,7 +920,7 @@ .monaco-workbench .notebookOverlay > .cell-list-container .notebook-folding-indicator .codicon { visibility: visible; height: 16px; - padding: 4px 4px 4px 6px; + padding: 4px 4px 4px 4px; } /** Theming */ @@ -954,3 +1009,4 @@ .hc-black .notebookOverlay .monaco-list.selection-multiple:focus-within .monaco-list-row.selected:not(.focused) .cell-focus-indicator-bottom:before { border-bottom-style: dotted; } .hc-black .notebookOverlay .monaco-list.selection-multiple:focus-within .monaco-list-row.selected:not(.focused) .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-left:before { border-left-style: dotted; } .hc-black .notebookOverlay .monaco-list.selection-multiple:focus-within .monaco-list-row.selected:not(.focused) .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-right:before { border-right-style: dotted; } + diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebookKernelActionViewItem.css b/src/vs/workbench/contrib/notebook/browser/media/notebookKernelActionViewItem.css new file mode 100644 index 0000000000..f8d01ec777 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/media/notebookKernelActionViewItem.css @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .kernel-action-view-item { + border-radius: 5px; +} +.monaco-workbench .kernel-action-view-item:hover { + background-color: var(--code-toolbarHoverBackground); +} + +.monaco-workbench .kernel-action-view-item .action-label { + display: inline-flex; +} + +.monaco-workbench .kernel-action-view-item .kernel-label { + font-size: 11px; + padding: 3px 5px 3px 3px; + border-radius: 5px; + height: 16px; + display: inline-flex; + vertical-align: text-bottom; +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index e818bdd26e..09edf0cb2c 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -4,17 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import { Schemas } from 'vs/base/common/network'; -import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { parse } from 'vs/base/common/marshalling'; import { isEqual } from 'vs/base/common/resources'; import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; +import { format } from 'vs/base/common/jsonFormatter'; +import { applyEdits } from 'vs/base/common/jsonEdit'; import { ITextModel, ITextBufferFactory, DefaultEndOfLine, ITextBuffer } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; import * as nls from 'vs/nls'; -// import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; {{SQL CARBON EDIT}} Remove VS Notebook configurations +import { IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; // {{SQL CARBON EDIT}} Remove VS Notebook configurations import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -22,13 +24,13 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { Registry } from 'vs/platform/registry/common/platform'; import { EditorDescriptor, IEditorRegistry } from 'vs/workbench/browser/editor'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { EditorInput, ICustomEditorInputFactory, IEditorInput, IEditorInputSerializer, IEditorInputFactoryRegistry, IEditorInputWithOptions, EditorExtensions } from 'vs/workbench/common/editor'; -import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { IEditorInput, IEditorInputSerializer, IEditorInputFactoryRegistry, IEditorInputWithOptions, EditorExtensions } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookServiceImpl'; -import { CellKind, CellUri, getCellUndoRedoComparisonKey, IResolvedNotebookEditorModel, NotebookDocumentBackupData, NOTEBOOK_WORKING_COPY_TYPE_PREFIX } from 'vs/workbench/contrib/notebook/common/notebookCommon'; // {{SQL CARBON EDIT}} Remove VS Notebook configurations +import { CellKind, CellUri, UndoRedoPerCell, getCellUndoRedoComparisonKey, IResolvedNotebookEditorModel, NotebookDocumentBackupData, NotebookWorkingCopyTypeIdentifier } from 'vs/workbench/contrib/notebook/common/notebookCommon'; // {{SQL CARBON EDIT}} Remove VS Notebook configurations import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; @@ -41,16 +43,22 @@ import { NotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/brow import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; import { NotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorServiceImpl'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; -import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; import { Event } from 'vs/base/common/event'; import { getFormatedMetadataJSON } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; import { NotebookModelResolverServiceImpl } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookKernelService } from 'vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl'; -import { IWorkingCopyIdentifier, NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { EditorOverride } from 'vs/platform/editor/common/editor'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { NotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl'; +import { INotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; // Editor Contribution import 'vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard'; @@ -58,10 +66,12 @@ 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/gettingStarted/notebookGettingStarted'; import 'vs/workbench/contrib/notebook/browser/contrib/layout/layoutActions'; import 'vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider'; import 'vs/workbench/contrib/notebook/browser/contrib/navigation/arrow'; import 'vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline'; +import 'vs/workbench/contrib/notebook/browser/contrib/profile/notebookProfile'; import 'vs/workbench/contrib/notebook/browser/contrib/statusBar/statusBarProviders'; import 'vs/workbench/contrib/notebook/browser/contrib/statusBar/contributedStatusBarItemController'; import 'vs/workbench/contrib/notebook/browser/contrib/statusBar/executionStatusBarItemController'; @@ -71,12 +81,12 @@ import 'vs/workbench/contrib/notebook/browser/contrib/cellOperations/cellOperati import 'vs/workbench/contrib/notebook/browser/contrib/viewportCustomMarkdown/viewportCustomMarkdown'; import 'vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout'; - // Diff Editor Contribution import 'vs/workbench/contrib/notebook/browser/diff/notebookDiffActions'; // Output renderers registration import 'vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform'; +import { editorOptionsRegistry } from 'vs/editor/common/config/editorOptions'; /*--------------------------------------------------------------------------------------------- */ @@ -120,7 +130,7 @@ class NotebookDiffEditorSerializer implements IEditorInputSerializer { } deserialize(instantiationService: IInstantiationService, raw: string) { - type Data = { resource: URI, originalResource: URI, name: string, originalName: string, viewType: string, textDiffName: string | undefined, group: number }; + type Data = { resource: URI, originalResource: URI, name: string, originalName: string, viewType: string, textDiffName: string | undefined, group: number; }; const data = parse(raw); if (!data) { return undefined; @@ -154,7 +164,7 @@ class NotebookEditorSerializer implements IEditorInputSerializer { }); } deserialize(instantiationService: IInstantiationService, raw: string) { - type Data = { resource: URI, viewType: string, group: number }; + type Data = { resource: URI, viewType: string, group: number; }; const data = parse(raw); if (!data) { return undefined; @@ -174,35 +184,6 @@ Registry.as(EditorExtensions.EditorInputFactories). NotebookEditorSerializer ); -Registry.as(EditorExtensions.EditorInputFactories).registerCustomEditorInputFactory( - Schemas.vscodeNotebook, - new class implements ICustomEditorInputFactory { - async createCustomEditorInput(resource: URI, instantiationService: IInstantiationService): Promise { - return instantiationService.invokeFunction(async accessor => { - const workingCopyBackupService = accessor.get(IWorkingCopyBackupService); - - const backup = await workingCopyBackupService.resolve({ resource, typeId: NO_TYPE_ID }); - if (!backup?.meta) { - throw new Error(`No backup found for Notebook editor: ${resource}`); - } - - const input = NotebookEditorInput.create(instantiationService, resource, backup.meta.viewType, { startDirty: true }); - return input; - }); - } - - canResolveBackup(editorInput: IEditorInput, backupResource: URI): boolean { - if (editorInput instanceof NotebookEditorInput) { - if (isEqual(URI.from({ scheme: Schemas.vscodeNotebook, path: editorInput.resource.toString() }), backupResource)) { - return true; - } - } - - return false; - } - } -); - Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputSerializer( NotebookDiffEditorInput.ID, NotebookDiffEditorSerializer @@ -211,12 +192,15 @@ Registry.as(EditorExtensions.EditorInputFactories). export class NotebookContribution extends Disposable implements IWorkbenchContribution { constructor( @IUndoRedoService undoRedoService: IUndoRedoService, + @IConfigurationService configurationService: IConfigurationService, ) { super(); + const undoRedoPerCell = configurationService.getValue(UndoRedoPerCell); + this._register(undoRedoService.registerUriComparisonKeyComputer(CellUri.scheme, { getComparisonKey: (uri: URI): string => { - return getCellUndoRedoComparisonKey(uri); + return getCellUndoRedoComparisonKey(uri, undoRedoPerCell); } })); } @@ -265,7 +249,7 @@ class CellContentProvider implements ITextModelContentProvider { return cell.textBuffer.getLineContent(1).substr(0, limit); } }; - const language = cell.cellKind === CellKind.Markdown ? this._modeService.create('markdown') : (cell.language ? this._modeService.create(cell.language) : this._modeService.createByFilepathOrFirstLine(resource, cell.textBuffer.getLineContent(1))); + const language = cell.cellKind === CellKind.Markup ? this._modeService.create('markdown') : (cell.language ? this._modeService.create(cell.language) : this._modeService.createByFilepathOrFirstLine(resource, cell.textBuffer.getLineContent(1))); result = this._modelService.createModel( bufferFactory, language, @@ -275,6 +259,84 @@ class CellContentProvider implements ITextModelContentProvider { } } + if (result) { + const once = Event.any(result.onWillDispose, ref.object.notebook.onWillDispose)(() => { + once.dispose(); + ref.dispose(); + }); + } + + return result; + } +} + +class CellInfoContentProvider { + private readonly _disposables: IDisposable[] = []; + + constructor( + @ITextModelService textModelService: ITextModelService, + @IModelService private readonly _modelService: IModelService, + @IModeService private readonly _modeService: IModeService, + @ILabelService private readonly _labelService: ILabelService, + @INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService, + ) { + this._disposables.push(textModelService.registerTextModelContentProvider(Schemas.vscodeNotebookCellMetadata, { + provideTextContent: this.provideMetadataTextContent.bind(this) + })); + + this._disposables.push(textModelService.registerTextModelContentProvider(Schemas.vscodeNotebookCellOutput, { + provideTextContent: this.provideOutputTextContent.bind(this) + })); + + this._disposables.push(this._labelService.registerFormatter({ + scheme: Schemas.vscodeNotebookCellMetadata, + formatting: { + label: '${path} (metadata)', + separator: '/' + } + })); + + this._disposables.push(this._labelService.registerFormatter({ + scheme: Schemas.vscodeNotebookCellOutput, + formatting: { + label: '${path} (output)', + separator: '/' + } + })); + } + + dispose(): void { + this._disposables.forEach(d => d.dispose()); + } + + async provideMetadataTextContent(resource: URI): Promise { + const existing = this._modelService.getModel(resource); + if (existing) { + return existing; + } + + const data = CellUri.parseCellUri(resource, Schemas.vscodeNotebookCellMetadata); + 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(); @@ -284,31 +346,14 @@ class CellContentProvider implements ITextModelContentProvider { return result; } -} -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 { + async provideOutputTextContent(resource: URI): Promise { const existing = this._modelService.getModel(resource); if (existing) { return existing; } - const data = CellUri.parseCellMetadataUri(resource); - // const data = parseCellUri(resource); + + const data = CellUri.parseCellUri(resource, Schemas.vscodeNotebookCellOutput); if (!data) { return null; } @@ -320,9 +365,11 @@ class CellMetadataContentProvider implements ITextModelContentProvider { for (const cell of ref.object.notebook.cells) { if (cell.handle === data.handle) { - const metadataSource = getFormatedMetadataJSON(ref.object.notebook, cell.metadata || {}, cell.language); + const content = JSON.stringify(cell.outputs); + const edits = format(content, undefined, {}); + const outputSource = applyEdits(content, edits); result = this._modelService.createModel( - metadataSource, + outputSource, mode, resource ); @@ -374,27 +421,37 @@ class RegisterSchemasContribution extends Disposable implements IWorkbenchContri } } -// makes sure that every dirty notebook gets an editor -class NotebookFileTracker implements IWorkbenchContribution { +class NotebookEditorManager implements IWorkbenchContribution { - private readonly _dirtyListener: IDisposable; + private readonly _disposables = new DisposableStore(); constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, @IEditorService private readonly _editorService: IEditorService, @INotebookEditorModelResolverService private readonly _notebookEditorModelService: INotebookEditorModelResolverService, + @INotebookService notebookService: INotebookService, + @IEditorGroupsService editorGroups: IEditorGroupsService, ) { + // OPEN notebook editor for models that have turned dirty without being visible in an editor type E = IResolvedNotebookEditorModel; - this._dirtyListener = Event.debounce( + this._disposables.add(Event.debounce( this._notebookEditorModelService.onDidChangeDirty, (last, current) => !last ? [current] : [...last, current], 100 - )(this._openMissingDirtyNotebookEditors, this); + )(this._openMissingDirtyNotebookEditors, this)); + + // CLOSE notebook editor for models that have no more serializer + this._disposables.add(notebookService.onWillRemoveViewType(e => { + for (const group of editorGroups.groups) { + const staleInputs = group.editors.filter(input => input instanceof NotebookEditorInput && input.viewType === e); + group.closeEditors(staleInputs); + } + })); } dispose(): void { - this._dirtyListener.dispose(); + this._disposables.dispose(); } private _openMissingDirtyNotebookEditors(models: IResolvedNotebookEditorModel[]): void { @@ -413,7 +470,7 @@ class NotebookFileTracker implements IWorkbenchContribution { } } -class NotebookWorkingCopyEditorHandler extends Disposable implements IWorkbenchContribution { +class SimpleNotebookWorkingCopyEditorHandler extends Disposable implements IWorkbenchContribution { constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -429,28 +486,62 @@ class NotebookWorkingCopyEditorHandler extends Disposable implements IWorkbenchC await this._extensionService.whenInstalledExtensionsRegistered(); this._register(this._workingCopyEditorService.registerHandler({ - handles: workingCopy => typeof this.getViewType(workingCopy) === 'string', - isOpen: (workingCopy, editor) => editor instanceof NotebookEditorInput && editor.viewType === this.getViewType(workingCopy), - createEditor: workingCopy => NotebookEditorInput.create(this._instantiationService, workingCopy.resource, this.getViewType(workingCopy)!) + handles: workingCopy => typeof this._getViewType(workingCopy) === 'string', + isOpen: (workingCopy, editor) => editor instanceof NotebookEditorInput && editor.viewType === this._getViewType(workingCopy), + createEditor: workingCopy => NotebookEditorInput.create(this._instantiationService, workingCopy.resource, this._getViewType(workingCopy)!) })); } - private getViewType(workingCopy: IWorkingCopyIdentifier): string | undefined { - if (workingCopy.typeId.startsWith(NOTEBOOK_WORKING_COPY_TYPE_PREFIX)) { - return workingCopy.typeId.substr(NOTEBOOK_WORKING_COPY_TYPE_PREFIX.length); - } + private _getViewType(workingCopy: IWorkingCopyIdentifier): string | undefined { + return NotebookWorkingCopyTypeIdentifier.parse(workingCopy.typeId); + } +} - return undefined; +class ComplexNotebookWorkingCopyEditorHandler extends Disposable implements IWorkbenchContribution { + + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IWorkingCopyEditorService private readonly _workingCopyEditorService: IWorkingCopyEditorService, + @IExtensionService private readonly _extensionService: IExtensionService, + @IWorkingCopyBackupService private readonly _workingCopyBackupService: IWorkingCopyBackupService + ) { + super(); + + this._installHandler(); + } + + private async _installHandler(): Promise { + await this._extensionService.whenInstalledExtensionsRegistered(); + + this._register(this._workingCopyEditorService.registerHandler({ + handles: workingCopy => workingCopy.resource.scheme === Schemas.vscodeNotebook, + isOpen: (workingCopy, editor) => editor instanceof NotebookEditorInput && isEqual(URI.from({ scheme: Schemas.vscodeNotebook, path: editor.resource.toString() }), workingCopy.resource), + createEditor: async workingCopy => { + // TODO this is really bad and should adopt the `typeId` + // for backups instead of storing that information in the + // backup. + // But since complex notebooks are deprecated, not worth + // pushing for it and should eventually delete this code + // entirely. + const backup = await this._workingCopyBackupService.resolve(workingCopy); + if (!backup?.meta) { + throw new Error(`No backup found for Notebook editor: ${workingCopy.resource}`); + } + + return NotebookEditorInput.create(this._instantiationService, workingCopy.resource, backup.meta.viewType, { startDirty: true }); + } + })); } } const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(NotebookContribution, LifecyclePhase.Starting); workbenchContributionsRegistry.registerWorkbenchContribution(CellContentProvider, LifecyclePhase.Starting); -workbenchContributionsRegistry.registerWorkbenchContribution(CellMetadataContentProvider, LifecyclePhase.Starting); +workbenchContributionsRegistry.registerWorkbenchContribution(CellInfoContentProvider, LifecyclePhase.Starting); workbenchContributionsRegistry.registerWorkbenchContribution(RegisterSchemasContribution, LifecyclePhase.Starting); -workbenchContributionsRegistry.registerWorkbenchContribution(NotebookFileTracker, LifecyclePhase.Ready); -workbenchContributionsRegistry.registerWorkbenchContribution(NotebookWorkingCopyEditorHandler, LifecyclePhase.Ready); +workbenchContributionsRegistry.registerWorkbenchContribution(NotebookEditorManager, LifecyclePhase.Ready); +workbenchContributionsRegistry.registerWorkbenchContribution(SimpleNotebookWorkingCopyEditorHandler, LifecyclePhase.Ready); +workbenchContributionsRegistry.registerWorkbenchContribution(ComplexNotebookWorkingCopyEditorHandler, LifecyclePhase.Ready); registerSingleton(INotebookService, NotebookService); registerSingleton(INotebookEditorWorkerService, NotebookEditorWorkerServiceImpl); @@ -458,8 +549,47 @@ registerSingleton(INotebookEditorModelResolverService, NotebookModelResolverServ registerSingleton(INotebookCellStatusBarService, NotebookCellStatusBarService, true); registerSingleton(INotebookEditorService, NotebookEditorWidgetService, true); registerSingleton(INotebookKernelService, NotebookKernelService, true); +registerSingleton(INotebookRendererMessagingService, NotebookRendererMessagingService, true); + +const schemas: IJSONSchemaMap = {}; +function isConfigurationPropertySchema(x: IConfigurationPropertySchema | { [path: string]: IConfigurationPropertySchema; }): x is IConfigurationPropertySchema { + return (typeof x.type !== 'undefined' || typeof x.anyOf !== 'undefined'); +} +for (const editorOption of editorOptionsRegistry) { + const schema = editorOption.schema; + if (schema) { + if (isConfigurationPropertySchema(schema)) { + schemas[`editor.${editorOption.name}`] = schema; + } else { + for (let key in schema) { + if (Object.hasOwnProperty.call(schema, key)) { + schemas[key] = schema[key]; + } + } + } + } +} /* {{SQL CARBON EDIT}} Remove VS Notebook configurations +const editorOptionsCustomizationSchema: IConfigurationPropertySchema = { + description: nls.localize('notebook.editorOptions.experimentalCustomization', 'Settings for code editors used in notebooks. This can be used to customize most editor.* settings.'), + default: {}, + allOf: [ + { + properties: schemas, + } + // , { + // patternProperties: { + // '^\\[.*\\]$': { + // type: 'object', + // default: {}, + // properties: schemas + // } + // } + // } + ] +}; + const configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration({ id: 'notebook', @@ -475,7 +605,7 @@ configurationRegistry.registerConfiguration({ }, default: [] }, - [CellToolbarLocKey]: { + [CellToolbarLocation]: { description: nls.localize('notebook.cellToolbarLocation.description', "Where the cell toolbar should be shown, or whether it should be hidden."), type: 'object', additionalProperties: { @@ -485,12 +615,19 @@ configurationRegistry.registerConfiguration({ }, default: { 'default': 'right' - } + }, + tags: ['notebookLayout'] }, - [ShowCellStatusBarKey]: { + [ShowCellStatusBar]: { description: nls.localize('notebook.showCellStatusbar.description', "Whether the cell status bar should be shown."), - type: 'boolean', - default: true + type: 'string', + enum: ['hidden', 'visible', 'visibleAfterExecute'], + enumDescriptions: [ + nls.localize('notebook.showCellStatusbar.hidden.description', "The cell status bar is always hidden."), + nls.localize('notebook.showCellStatusbar.visible.description', "The cell status bar is always visible."), + nls.localize('notebook.showCellStatusbar.visibleAfterExecute.description', "The cell status bar is hidden until the cell has executed. Then it becomes visible to show the execution status.")], + default: 'visible', + tags: ['notebookLayout'] }, [NotebookTextDiffEditorPreview]: { description: nls.localize('notebook.diff.enablePreview.description', "Whether to use the enhanced text diff editor for notebook."), @@ -502,6 +639,70 @@ configurationRegistry.registerConfiguration({ type: 'boolean', default: true }, + [CellToolbarVisibility]: { + markdownDescription: nls.localize('notebook.cellToolbarVisibility.description', "Whether the cell toolbar should appear on hover or click."), + type: 'string', + enum: ['hover', 'click'], + default: 'click', + tags: ['notebookLayout'] + }, + [UndoRedoPerCell]: { + description: nls.localize('notebook.undoRedoPerCell.description', "Whether to use separate undo/redo stack for each cell."), + type: 'boolean', + default: false + }, + [CompactView]: { + description: nls.localize('notebook.compactView.description', "Control whether the notebook editor should be rendered in a compact form. "), + type: 'boolean', + default: true, + tags: ['notebookLayout'] + }, + [FocusIndicator]: { + description: nls.localize('notebook.focusIndicator.description', "Control whether to render the focus indicator as cell borders or a highlight bar on the left gutter"), + type: 'string', + enum: ['border', 'gutter'], + default: 'gutter', + tags: ['notebookLayout'] + }, + [InsertToolbarLocation]: { + description: nls.localize('notebook.insertToolbarPosition.description', "Control where the insert cell actions should be rendered."), + type: 'string', + enum: ['betweenCells', 'notebookToolbar', 'both', 'hidden'], + default: 'both', + tags: ['notebookLayout'] + }, + [GlobalToolbar]: { + description: nls.localize('notebook.globalToolbar.description', "Control whether to render a global toolbar inside the notebook editor."), + type: 'boolean', + default: true, + tags: ['notebookLayout'] + }, + [ConsolidatedOutputButton]: { + description: nls.localize('notebook.consolidatedOutputButton.description', "Control whether outputs action should be rendered in the output toolbar."), + type: 'boolean', + default: true, + tags: ['notebookLayout'] + }, + [ShowFoldingControls]: { + description: nls.localize('notebook.showFoldingControls.description', "Controls when the folding controls are shown."), + type: 'string', + enum: ['always', 'mouseover'], + default: 'mouseover', + tags: ['notebookLayout'] + }, + [DragAndDropEnabled]: { + description: nls.localize('notebook.dragAndDrop.description', "Control whether the notebook editor should allow moving cells through drag and drop."), + type: 'boolean', + default: true, + tags: ['notebookLayout'] + }, + [ConsolidatedRunButton]: { + description: nls.localize('notebook.consolidatedRunButton.description', "Control whether extra actions are shown in a dropdown next to the run button."), + type: 'boolean', + default: true, + tags: ['notebookLayout'] + }, + [NotebookCellEditorOptionsCustomizations]: editorOptionsCustomizationSchema } }); */ diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 965a0de570..0c8065c848 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -8,7 +8,7 @@ import { IListContextMenuEvent, IListEvent, IListMouseEvent } from 'vs/base/brow import { IListOptions, IListStyles } from 'vs/base/browser/ui/list/listWidget'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ScrollEvent } from 'vs/base/common/scrollable'; import { URI } from 'vs/base/common/uri'; @@ -16,26 +16,28 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; import { IPosition } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { FindMatch, IReadonlyTextBuffer, ITextModel } from 'vs/editor/common/model'; +import { FindMatch, IModelDeltaDecoration, IReadonlyTextBuffer, ITextModel } from 'vs/editor/common/model'; import { ContextKeyExpr, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, NotebookCellMetadata, INotebookKernel, IOrderedMimeType, INotebookRendererInfo, ICellOutput, IOutputItemDto, INotebookCellStatusBarItem } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, NotebookCellMetadata, INotebookKernel, IOrderedMimeType, INotebookRendererInfo, ICellOutput, IOutputItemDto, INotebookCellStatusBarItem, NotebookCellInternalMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange, cellRangesToIndexes, reduceRanges } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { IMenu } from 'vs/platform/actions/common/actions'; -import { EditorOptions, IEditorPane } from 'vs/workbench/common/editor'; -import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { IEditorPane } from 'vs/workbench/common/editor'; +import { ITextEditorOptions, ITextResourceEditorInput } 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'; import { INotebookWebviewMessage } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; +import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; export const NOTEBOOK_EDITOR_ID = 'workbench.editor.notebook'; export const NOTEBOOK_DIFF_EDITOR_ID = 'workbench.editor.notebookTextDiffEditor'; //#region Context Keys +export const HAS_OPENED_NOTEBOOK = new RawContextKey('userHasOpenedNotebook', false); export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey('notebookFindWidgetFocused', false); // Is Notebook @@ -48,10 +50,11 @@ export const NOTEBOOK_CELL_LIST_FOCUSED = new RawContextKey('notebookCe export const NOTEBOOK_OUTPUT_FOCUSED = new RawContextKey('notebookOutputFocused', false); export const NOTEBOOK_EDITOR_EDITABLE = new RawContextKey('notebookEditable', true); export const NOTEBOOK_HAS_RUNNING_CELL = new RawContextKey('notebookHasRunningCell', false); +export const NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON = new RawContextKey('notebookUseConsolidatedOutputButton', false); // Cell keys -export const NOTEBOOK_VIEW_TYPE = new RawContextKey('notebookViewType', undefined); -export const NOTEBOOK_CELL_TYPE = new RawContextKey('notebookCellType', undefined); // code, markdown +export const NOTEBOOK_VIEW_TYPE = new RawContextKey('notebookType', undefined); +export const NOTEBOOK_CELL_TYPE = new RawContextKey<'code' | 'markup'>('notebookCellType', undefined); export const NOTEBOOK_CELL_EDITABLE = new RawContextKey('notebookCellEditable', false); // bool export const NOTEBOOK_CELL_FOCUSED = new RawContextKey('notebookCellFocused', false); // bool export const NOTEBOOK_CELL_EDITOR_FOCUSED = new RawContextKey('notebookCellEditorFocused', false); // bool @@ -64,8 +67,11 @@ export const NOTEBOOK_CELL_INPUT_COLLAPSED = new RawContextKey('noteboo export const NOTEBOOK_CELL_OUTPUT_COLLAPSED = new RawContextKey('notebookCellOutputIsCollapsed', false); // bool // Kernels export const NOTEBOOK_KERNEL_COUNT = new RawContextKey('notebookKernelCount', 0); +export const NOTEBOOK_KERNEL_SELECTED = new RawContextKey('notebookKernelSelected', false); export const NOTEBOOK_INTERRUPTIBLE_KERNEL = new RawContextKey('notebookInterruptibleKernel', false); +export const NOTEBOOK_HAS_OUTPUTS = new RawContextKey('notebookHasOutputs', false); + //#endregion //#region Shared commands @@ -87,6 +93,7 @@ export interface IRenderMainframeOutput { type: RenderOutputType.Mainframe; supportAppend?: boolean; initHeight?: number; + disposable?: IDisposable; } export interface IRenderPlainHtmlOutput { @@ -112,14 +119,14 @@ export interface ICellOutputViewModel { */ model: ICellOutput; resolveMimeTypes(textModel: NotebookTextModel, kernelProvides: readonly string[] | undefined): [readonly IOrderedMimeType[], number]; - pickedMimeType: number; + pickedMimeType: IOrderedMimeType | undefined; supportAppend(): boolean; + hasMultiMimeType(): boolean; toRawJSON(): any; } export interface IDisplayOutputViewModel extends ICellOutputViewModel { resolveMimeTypes(textModel: NotebookTextModel, kernelProvides: readonly string[] | undefined): [readonly IOrderedMimeType[], number]; - pickedMimeType: number; } @@ -131,7 +138,7 @@ export interface IGenericCellViewModel { id: string; handle: number; uri: URI; - metadata: NotebookCellMetadata | undefined; + metadata: NotebookCellMetadata; outputIsHovered: boolean; outputIsFocused: boolean; outputsViewModels: ICellOutputViewModel[]; @@ -168,16 +175,16 @@ export interface ICommonNotebookEditor { triggerScroll(event: IMouseWheelEvent): void; getCellByInfo(cellInfo: ICommonCellInfo): IGenericCellViewModel; getCellById(cellId: string): IGenericCellViewModel | undefined; - toggleNotebookCellSelection(cell: IGenericCellViewModel): void; + toggleNotebookCellSelection(cell: IGenericCellViewModel, selectFromPrevious: boolean): void; focusNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output', options?: IFocusNotebookCellOptions): void; focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): void; updateOutputHeight(cellInfo: ICommonCellInfo, output: IDisplayOutputViewModel, height: number, isInit: boolean, source?: string): void; scheduleOutputHeightAck(cellInfo: ICommonCellInfo, outputId: string, height: number): void; updateMarkdownCellHeight(cellId: string, height: number, isInit: boolean): void; setMarkdownCellEditState(cellId: string, editState: CellEditState): void; - markdownCellDragStart(cellId: string, position: { clientY: number }): void; - markdownCellDrag(cellId: string, position: { clientY: number }): void; - markdownCellDrop(cellId: string, position: { clientY: number, ctrlKey: boolean, altKey: boolean }): void; + markdownCellDragStart(cellId: string, event: { dragOffsetY: number }): void; + markdownCellDrag(cellId: string, event: { dragOffsetY: number }): void; + markdownCellDrop(cellId: string, event: { dragOffsetY: number, ctrlKey: boolean, altKey: boolean }): void; markdownCellDragEnd(cellId: string): void; } @@ -230,6 +237,7 @@ export interface MarkdownCellLayoutInfo { readonly fontInfo: FontInfo | null; readonly editorWidth: number; readonly editorHeight: number; + readonly previewHeight: number; readonly bottomToolbarOffset: number; readonly totalHeight: number; } @@ -237,6 +245,8 @@ export interface MarkdownCellLayoutInfo { export interface MarkdownCellLayoutChangeEvent { font?: FontInfo; outerWidth?: number; + editorHeight?: number; + previewHeight?: number; totalHeight?: number; } @@ -248,6 +258,7 @@ export interface ICellViewModel extends IGenericCellViewModel { readonly onDidChangeLayout: Event<{ totalHeight?: boolean | number; outerWidth?: number; }>; readonly onDidChangeCellStatusBarItems: Event; readonly editStateSource: string; + readonly editorAttached: boolean; dragging: boolean; handle: number; uri: URI; @@ -259,7 +270,8 @@ export interface ICellViewModel extends IGenericCellViewModel { getText(): string; getTextLength(): number; getHeight(lineHeight: number): number; - metadata: NotebookCellMetadata | undefined; + metadata: NotebookCellMetadata; + internalMetadata: NotebookCellInternalMetadata; textModel: ITextModel | undefined; hasModel(): this is IEditableCellViewModel; resolveTextModel(): Promise; @@ -268,6 +280,8 @@ export interface ICellViewModel extends IGenericCellViewModel { getCellStatusBarItems(): INotebookCellStatusBarItem[]; getEditState(): CellEditState; updateEditState(state: CellEditState, source: string): void; + deltaModelDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[]; + getCellDecorationRange(id: string): Range | null; } export interface IEditableCellViewModel extends ICellViewModel { @@ -311,23 +325,10 @@ export interface INotebookDeltaCellStatusBarItems { items: INotebookCellStatusBarItem[]; } -export class NotebookEditorOptions extends EditorOptions { - - readonly cellOptions?: IResourceEditorInput; +export interface INotebookEditorOptions extends ITextEditorOptions { + readonly cellOptions?: ITextResourceEditorInput; readonly cellSelections?: ICellRange[]; readonly isReadOnly?: boolean; - - constructor(options: Partial) { - super(); - this.overwrite(options); - this.cellOptions = options.cellOptions; - this.cellSelections = options.cellSelections; - this.isReadOnly = options.isReadOnly; - } - - with(options: Partial): NotebookEditorOptions { - return new NotebookEditorOptions({ ...this, ...options }); - } } export type INotebookEditorContributionCtor = IConstructorSignature1; @@ -344,6 +345,7 @@ export interface INotebookEditorCreationOptions { export interface IActiveNotebookEditor extends INotebookEditor { viewModel: NotebookViewModel; + textModel: NotebookTextModel; getFocus(): ICellRange; } @@ -377,6 +379,7 @@ export interface INotebookEditor extends ICommonNotebookEditor { readonly onDidScroll: Event; readonly onDidChangeActiveCell: Event; + readonly notebookOptions: NotebookOptions; isDisposed: boolean; dispose(): void; @@ -395,7 +398,7 @@ export interface INotebookEditor extends ICommonNotebookEditor { hasWebviewFocus(): boolean; hasOutputTextSelection(): boolean; - setOptions(options: NotebookEditorOptions | undefined): Promise; + setOptions(options: INotebookEditorOptions | undefined): Promise; /** * Select & focus cell @@ -498,7 +501,7 @@ export interface INotebookEditor extends ICommonNotebookEditor { /** * Send message to the webview for outputs. */ - postMessage(forRendererId: string | undefined, message: any): void; + postMessage(message: any): void; /** * Remove class name on the notebook editor root DOM node. @@ -761,7 +764,7 @@ export interface IOutputTransformContribution { * This call is allowed to have side effects, such as placing output * directly into the container element. */ - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput; + render(output: ICellOutputViewModel, item: IOutputItemDto, container: HTMLElement, notebookUri: URI): IRenderOutput; } export interface CellFindMatch { @@ -769,6 +772,12 @@ export interface CellFindMatch { matches: FindMatch[]; } +export interface CellFindMatchWithIndex { + cell: CellViewModel; + index: number; + matches: FindMatch[]; +} + export enum CellRevealType { Line, Range @@ -809,6 +818,7 @@ export enum CursorAtBoundary { export interface CellViewModelStateChangeEvent { readonly metadataChanged?: boolean; + readonly internalMetadataChanged?: boolean; readonly runStateChanged?: boolean; readonly selectionChanged?: boolean; readonly focusModeChanged?: boolean; @@ -897,20 +907,6 @@ export function getNotebookEditorFromEditorPane(editorPane?: IEditorPane): INote return editorPane?.getId() === NOTEBOOK_EDITOR_ID ? editorPane.getControl() as INotebookEditor | undefined : undefined; } -let EDITOR_TOP_PADDING = 12; -const editorTopPaddingChangeEmitter = new Emitter(); - -export const EditorTopPaddingChangeEvent = editorTopPaddingChangeEmitter.event; - -export function updateEditorTopPadding(top: number) { - EDITOR_TOP_PADDING = top; - editorTopPaddingChangeEmitter.fire(); -} - -export function getEditorTopPadding() { - return EDITOR_TOP_PADDING; -} - /** * ranges: model selections * this will convert model selections to view indexes first, and then include the hidden ranges in the list view diff --git a/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts index b424a11a61..811f18b13d 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts @@ -10,7 +10,6 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle' import { URI } from 'vs/base/common/uri'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; import { INotebookCellStatusBarItemList, INotebookCellStatusBarItemProvider } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { score } from 'vs/workbench/contrib/notebook/common/notebookSelector'; export class NotebookCellStatusBarService extends Disposable implements INotebookCellStatusBarService { @@ -43,7 +42,7 @@ export class NotebookCellStatusBarService extends Disposable implements INoteboo } async getStatusBarItemsForCell(docUri: URI, cellIndex: number, viewType: string, token: CancellationToken): Promise { - const providers = this._providers.filter(p => score(p.selector, docUri, viewType) > 0); + const providers = this._providers.filter(p => p.viewType === viewType || p.viewType === '*'); return await Promise.all(providers.map(async p => { try { return await p.provideCellStatusBarItems(docUri, cellIndex, token) ?? { items: [] }; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts index 6d1e5c4233..746fbbd58f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as glob from 'vs/base/common/glob'; -import { EditorInput, IEditorInput, GroupIdentifier, ISaveOptions, IMoveResult, IRevertOptions, EditorModel } from 'vs/workbench/common/editor'; +import { IEditorInput, GroupIdentifier, ISaveOptions, IMoveResult, IRevertOptions, EditorInputCapabilities, IResourceDiffEditorInput } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { URI } from 'vs/base/common/uri'; import { isEqual } from 'vs/base/common/resources'; @@ -14,6 +16,7 @@ import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebo import { IReference } from 'vs/base/common/lifecycle'; import { INotebookDiffEditorModel, IResolvedNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Schemas } from 'vs/base/common/network'; +import { FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files'; interface NotebookEditorInputOptions { startDirty?: boolean; @@ -68,7 +71,8 @@ export class NotebookDiffEditorInput extends EditorInput { public readonly options: NotebookEditorInputOptions, @INotebookService private readonly _notebookService: INotebookService, @INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService, - @IFileDialogService private readonly _fileDialogService: IFileDialogService + @IFileDialogService private readonly _fileDialogService: IFileDialogService, + @IFileService private readonly _fileService: IFileService ) { super(); this._defaultDirtyState = !!options.startDirty; @@ -78,6 +82,26 @@ export class NotebookDiffEditorInput extends EditorInput { return NotebookDiffEditorInput.ID; } + override get capabilities(): EditorInputCapabilities { + let capabilities = EditorInputCapabilities.None; + + if (this._modifiedTextModel?.object.resource.scheme === Schemas.untitled) { + capabilities |= EditorInputCapabilities.Untitled; + } + + if (this._modifiedTextModel) { + if (this._modifiedTextModel.object.isReadonly()) { + capabilities |= EditorInputCapabilities.Readonly; + } + } else { + if (this._fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly)) { + capabilities |= EditorInputCapabilities.Readonly; + } + } + + return capabilities; + } + override getName(): string { return this.textDiffName; } @@ -89,18 +113,10 @@ export class NotebookDiffEditorInput extends EditorInput { return this._modifiedTextModel.object.isDirty(); } - override isUntitled(): boolean { - return this._modifiedTextModel?.object.resource.scheme === Schemas.untitled; - } - - override isReadonly() { - return false; - } - override async save(group: GroupIdentifier, options?: ISaveOptions): Promise { if (this._modifiedTextModel) { - if (this.isUntitled()) { + if (this.hasCapability(EditorInputCapabilities.Untitled)) { return this.saveAs(group, options); } else { await this._modifiedTextModel.object.save(); @@ -117,7 +133,7 @@ export class NotebookDiffEditorInput extends EditorInput { return undefined; } - const provider = this._notebookService.getContributedNotebookProvider(this.viewType!); + const provider = this._notebookService.getContributedNotebookType(this.viewType!); if (!provider) { return undefined; @@ -158,7 +174,7 @@ ${patterns} // called when users rename a notebook document override rename(group: GroupIdentifier, target: URI): IMoveResult | undefined { if (this._modifiedTextModel) { - const contributedNotebookProviders = this._notebookService.getContributedNotebookProviders(target); + const contributedNotebookProviders = this._notebookService.getContributedNotebookTypes(target); if (contributedNotebookProviders.find(provider => provider.id === this._modifiedTextModel!.object.viewType)) { return this._move(group, target); @@ -199,8 +215,18 @@ ${patterns} return new NotebookDiffEditorModel(this._originalTextModel.object, this._modifiedTextModel.object); } + override asResourceEditorInput(group: GroupIdentifier): IResourceDiffEditorInput { + return { + originalInput: { resource: this.originalResource }, + modifiedInput: { resource: this.resource }, + options: { + override: this.viewType + } + }; + } + override matches(otherInput: unknown): boolean { - if (this === otherInput) { + if (super.matches(otherInput)) { return true; } if (otherInput instanceof NotebookDiffEditorInput) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 5258a1d38a..849ebc406d 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -6,7 +6,7 @@ import * as DOM from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import 'vs/css!./media/notebook'; import { localize } from 'vs/nls'; import { extname } from 'vs/base/common/resources'; @@ -18,17 +18,21 @@ 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 { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { EditorOptions, IEditorInput, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorInputCapabilities, IEditorInput, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; 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, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditorOptions, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { IBorrowValue, INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; import { clearMarks, getAndClearMarks, mark } from 'vs/workbench/contrib/notebook/common/notebookPerformance'; import { IFileService } from 'vs/platform/files/common/files'; +import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IAction } from 'vs/base/common/actions'; +import { SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { NotebooKernelActionViewItem } from 'vs/workbench/contrib/notebook/browser/notebookKernelActionViewItem'; const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; @@ -37,11 +41,13 @@ export class NotebookEditor extends EditorPane { private readonly _editorMemento: IEditorMemento; private readonly _groupListener = this._register(new DisposableStore()); - private readonly _widgetDisposableStore: DisposableStore = new DisposableStore(); + private readonly _widgetDisposableStore: DisposableStore = this._register(new DisposableStore()); private _widget: IBorrowValue = { value: undefined }; private _rootElement!: HTMLElement; private _dimension?: DOM.Dimension; + private readonly inputListener = this._register(new MutableDisposable()); + // todo@rebornix is there a reason that `super.fireOnDidFocus` isn't used? private readonly _onDidFocusWidget = this._register(new Emitter()); override get onDidFocus(): Event { return this._onDidFocusWidget.event; } @@ -65,13 +71,25 @@ export class NotebookEditor extends EditorPane { super(NotebookEditor.ID, telemetryService, themeService, storageService); this._editorMemento = this.getEditorMemento(_editorGroupService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY); - this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidFileSystemProviderChange(e.scheme))); - this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidFileSystemProviderChange(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidChangeFileSystemProvider(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidChangeFileSystemProvider(e.scheme))); } - private onDidFileSystemProviderChange(scheme: string): void { - if (this.input?.resource?.scheme === scheme && this._widget.value) { - this._widget.value.setOptions(new NotebookEditorOptions({ isReadOnly: this.input.isReadonly() })); + private onDidChangeFileSystemProvider(scheme: string): void { + if (this.input instanceof NotebookEditorInput && this.input.resource?.scheme === scheme) { + this.updateReadonly(this.input); + } + } + + private onDidChangeInputCapabilities(input: NotebookEditorInput): void { + if (this.input === input) { + this.updateReadonly(input); + } + } + + private updateReadonly(input: NotebookEditorInput): void { + if (this._widget.value) { + this._widget.value.setOptions({ isReadOnly: input.hasCapability(EditorInputCapabilities.Readonly) }); } } @@ -103,6 +121,14 @@ export class NotebookEditor extends EditorPane { return this._rootElement; } + override getActionViewItem(action: IAction): IActionViewItem | undefined { + if (action.id === SELECT_KERNEL_ID) { + // this is being disposed by the consumer + return this.instantiationService.createInstance(NotebooKernelActionViewItem, action, this); + } + return undefined; + } + override getControl(): NotebookEditorWidget | undefined { return this._widget.value; } @@ -137,14 +163,16 @@ export class NotebookEditor extends EditorPane { const activeElement = document.activeElement; const value = this._widget.value; - return !!value && (DOM.isAncestor(activeElement, (value.getDomNode() || DOM.isAncestor(activeElement, value.getOverflowContainerDomNode())))); + return !!value && (DOM.isAncestor(activeElement, (value.getDomNode() || DOM.isAncestor(activeElement, value.getOverflowContainerDomNode())))); // {{SQL CARBON EDIT}} } - override async setInput(input: NotebookEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override async setInput(input: NotebookEditorInput, options: INotebookEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { clearMarks(input.resource); mark(input.resource, 'startTime'); const group = this.group!; + this.inputListener.value = input.onDidChangeCapabilities(() => this.onDidChangeInputCapabilities(input)); + this._saveEditorViewState(this.input); this._widgetDisposableStore.clear(); @@ -193,8 +221,8 @@ export class NotebookEditor extends EditorPane { this._widget.value?.setParentContextKeyService(this._contextKeyService); await this._widget.value!.setModel(model.notebook, viewState); - const isReadonly = input.isReadonly(); - await this._widget.value!.setOptions(options instanceof NotebookEditorOptions ? options.with({ isReadOnly: isReadonly }) : new NotebookEditorOptions({ isReadOnly: isReadonly })); + const isReadOnly = input.hasCapability(EditorInputCapabilities.Readonly); + await this._widget.value!.setOptions({ ...options, isReadOnly }); this._widgetDisposableStore.add(this._widget.value!.onDidFocus(() => this._onDidFocusWidget.fire())); this._widgetDisposableStore.add(this._editorDropService.createEditorDropTarget(this._widget.value!.getDomNode(), { @@ -258,6 +286,8 @@ export class NotebookEditor extends EditorPane { } override clearInput(): void { + this.inputListener.clear(); + if (this._widget.value) { this._saveEditorViewState(this.input); this._widget.value.onWillHide(); @@ -265,10 +295,8 @@ export class NotebookEditor extends EditorPane { super.clearInput(); } - override setOptions(options: EditorOptions | undefined): void { - if (options instanceof NotebookEditorOptions) { - this._widget.value?.setOptions(options); - } + override setOptions(options: INotebookEditorOptions | undefined): void { + this._widget.value?.setOptions(options); super.setOptions(options); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorDecorations.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorDecorations.ts index a89025aad5..b5bc3c83a9 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorDecorations.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorDecorations.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorKernelManager.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorKernelManager.ts index 597865448d..0bd80b2d9a 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorKernelManager.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorKernelManager.ts @@ -1,15 +1,16 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; import { Disposable } from 'vs/base/common/lifecycle'; import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellKind, INotebookKernel, INotebookTextModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, INotebookKernel, INotebookTextModel, NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; +import { SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; export class NotebookEditorKernelManager extends Disposable { @@ -24,32 +25,19 @@ export class NotebookEditorKernelManager extends Disposable { getSelectedOrSuggestedKernel(notebook: INotebookTextModel): INotebookKernel | undefined { // returns SELECTED or the ONLY available kernel const info = this._notebookKernelService.getMatchingKernel(notebook); - if (info.selected) { - return info.selected; - } - if (info.all.length === 1) { - return info.all[0]; - } - return undefined; + return info.selected ?? info.suggested; } async executeNotebookCells(notebook: INotebookTextModel, cells: Iterable): Promise { const message = nls.localize('notebookRunTrust', "Executing a notebook cell will run code from this workspace."); - const trust = await this._workspaceTrustRequestService.requestWorkspaceTrust({ - modal: true, - message - }); + const trust = await this._workspaceTrustRequestService.requestWorkspaceTrust({ message }); if (!trust) { return; } - if (!notebook.metadata.trusted) { - return; - } - let kernel = this.getSelectedOrSuggestedKernel(notebook); if (!kernel) { - await this._commandService.executeCommand('notebook.selectKernel'); + await this._commandService.executeCommand(SELECT_KERNEL_ID); kernel = this.getSelectedOrSuggestedKernel(notebook); } @@ -59,7 +47,7 @@ export class NotebookEditorKernelManager extends Disposable { const cellHandles: number[] = []; for (const cell of cells) { - if (cell.cellKind !== CellKind.Code) { + if (cell.cellKind !== CellKind.Code || cell.internalMetadata.runState === NotebookCellExecutionState.Pending || cell.internalMetadata.runState === NotebookCellExecutionState.Executing) { continue; } if (!kernel.supportedLanguages.includes(cell.language)) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorServiceImpl.ts index f935d9fb97..4a8322d969 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorServiceImpl.ts @@ -52,6 +52,7 @@ export class NotebookEditorWidgetService implements INotebookEditorService { value.token = undefined; this._disposeWidget(value.widget); widgets.delete(e.editor.resource); + value.widget = (undefined); // unset the widget so that others that still hold a reference don't harm us })); listeners.push(group.onWillMoveEditor(e => { if (e.editor instanceof NotebookEditorInput) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts new file mode 100644 index 0000000000..b24aac7490 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts @@ -0,0 +1,230 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IAction, Separator } from 'vs/base/common/actions'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { IMenu, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { toolbarActiveBackground } from 'vs/platform/theme/common/colorRegistry'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { INotebookEditor, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebooKernelActionViewItem } from 'vs/workbench/contrib/notebook/browser/notebookKernelActionViewItem'; +import { ActionViewWithLabel } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellActionView'; +import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus'; +import { GlobalToolbar } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; + +export class NotebookEditorToolbar extends Disposable { + // private _editorToolbarContainer!: HTMLElement; + private _leftToolbarScrollable!: DomScrollableElement; + private _notebookTopLeftToolbarContainer!: HTMLElement; + private _notebookTopRightToolbarContainer!: HTMLElement; + private _notebookGlobalActionsMenu!: IMenu; + private _notebookLeftToolbar!: ToolBar; + private _notebookRightToolbar!: ToolBar; + private _useGlobalToolbar: boolean = false; + + private readonly _onDidChangeState = this._register(new Emitter()); + onDidChangeState: Event = this._onDidChangeState.event; + + get useGlobalToolbar(): boolean { + return this._useGlobalToolbar; + } + + private _pendingLayout: IDisposable | undefined; + + constructor( + readonly notebookEditor: INotebookEditor, + readonly contextKeyService: IContextKeyService, + readonly domNode: HTMLElement, + @IInstantiationService readonly instantiationService: IInstantiationService, + @IConfigurationService readonly configurationService: IConfigurationService, + @IContextMenuService readonly contextMenuService: IContextMenuService, + @IEditorService private readonly editorService: IEditorService, + @IKeybindingService private readonly keybindingService: IKeybindingService, + @optional(ITASExperimentService) private readonly experimentService: ITASExperimentService + ) { + super(); + + this._buildBody(); + + this._register(this.editorService.onDidActiveEditorChange(() => { + if (this.editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) { + const notebookEditor = this.editorService.activeEditorPane.getControl() as INotebookEditor; + if (notebookEditor === this.notebookEditor) { + // this is the active editor + this._showNotebookActionsinEditorToolbar(); + return; + } + } + })); + + this._reigsterNotebookActionsToolbar(); + } + + private _buildBody() { + this._notebookTopLeftToolbarContainer = document.createElement('div'); + this._notebookTopLeftToolbarContainer.classList.add('notebook-toolbar-left'); + this._leftToolbarScrollable = new DomScrollableElement(this._notebookTopLeftToolbarContainer, { + vertical: ScrollbarVisibility.Hidden, + horizontal: ScrollbarVisibility.Auto, + horizontalScrollbarSize: 3, + useShadows: false, + scrollYToX: true + }); + this._register(this._leftToolbarScrollable); + + DOM.append(this.domNode, this._leftToolbarScrollable.getDomNode()); + this._notebookTopRightToolbarContainer = document.createElement('div'); + this._notebookTopRightToolbarContainer.classList.add('notebook-toolbar-right'); + DOM.append(this.domNode, this._notebookTopRightToolbarContainer); + } + + private _reigsterNotebookActionsToolbar() { + const cellMenu = this.instantiationService.createInstance(CellMenus); + this._notebookGlobalActionsMenu = this._register(cellMenu.getNotebookToolbar(this.contextKeyService)); + this._register(this._notebookGlobalActionsMenu); + + this._useGlobalToolbar = this.configurationService.getValue(GlobalToolbar) ?? false; + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(GlobalToolbar)) { + this._useGlobalToolbar = this.configurationService.getValue(GlobalToolbar); + this._showNotebookActionsinEditorToolbar(); + } + })); + + const context = { + ui: true, + notebookEditor: this.notebookEditor + }; + + const actionProvider = (action: IAction) => { + if (action.id === SELECT_KERNEL_ID) { + // // this is being disposed by the consumer + return this.instantiationService.createInstance(NotebooKernelActionViewItem, action, this.notebookEditor); + } + + return action instanceof MenuItemAction ? this.instantiationService.createInstance(ActionViewWithLabel, action) : undefined; + }; + + this._notebookLeftToolbar = new ToolBar(this._notebookTopLeftToolbarContainer, this.contextMenuService, { + getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), + actionViewItemProvider: actionProvider, + renderDropdownAsChildElement: true + }); + this._register(this._notebookLeftToolbar); + this._notebookLeftToolbar.context = context; + + this._notebookRightToolbar = new ToolBar(this._notebookTopRightToolbarContainer, this.contextMenuService, { + getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), + actionViewItemProvider: actionProvider, + renderDropdownAsChildElement: true + }); + this._register(this._notebookRightToolbar); + this._notebookRightToolbar.context = context; + + this._showNotebookActionsinEditorToolbar(); + this._register(this._notebookGlobalActionsMenu.onDidChange(() => { + this._showNotebookActionsinEditorToolbar(); + })); + + if (this.experimentService) { + this.experimentService.getTreatment('nbtoolbarineditor').then(treatment => { + if (treatment === undefined) { + return; + } + if (this._useGlobalToolbar !== treatment) { + this._useGlobalToolbar = treatment; + this._showNotebookActionsinEditorToolbar(); + } + }); + } + } + + private _showNotebookActionsinEditorToolbar() { + // when there is no view model, just ignore. + if (!this.notebookEditor.hasModel()) { + return; + } + + if (!this._useGlobalToolbar) { + this.domNode.style.display = 'none'; + } else { + this._notebookLeftToolbar.setActions([], []); + const groups = this._notebookGlobalActionsMenu.getActions({ shouldForwardArgs: true, renderShortTitle: true }); + this.domNode.style.display = 'flex'; + const primaryLeftGroups = groups.filter(group => /^navigation/.test(group[0])); + let primaryActions: IAction[] = []; + primaryLeftGroups.sort((a, b) => { + if (a[0] === 'navigation') { + return 1; + } + + if (b[0] === 'navigation') { + return -1; + } + + return 0; + }).forEach((group, index) => { + primaryActions.push(...group[1]); + if (index < primaryLeftGroups.length - 1) { + primaryActions.push(new Separator()); + } + }); + const primaryRightGroup = groups.find(group => /^status/.test(group[0])); + const primaryRightActions = primaryRightGroup ? primaryRightGroup[1] : []; + const secondaryActions = groups.filter(group => !/^navigation/.test(group[0]) && !/^status/.test(group[0])).reduce((prev: (MenuItemAction | SubmenuItemAction)[], curr) => { prev.push(...curr[1]); return prev; }, []); + + this._notebookLeftToolbar.setActions(primaryActions, secondaryActions); + this._notebookRightToolbar.setActions(primaryRightActions, []); + this._updateScrollbar(); + } + + this._onDidChangeState.fire(); + } + + layout() { + this._updateScrollbar(); + } + + private _updateScrollbar() { + this._pendingLayout?.dispose(); + + this._pendingLayout = DOM.measure(() => { + DOM.measure(() => { // double RAF + this._leftToolbarScrollable.setRevealOnScroll(false); + this._leftToolbarScrollable.scanDomNode(); + this._leftToolbarScrollable.setRevealOnScroll(true); + }); + }); + } + + override dispose() { + this._pendingLayout?.dispose(); + super.dispose(); + } +} + +registerThemingParticipant((theme, collector) => { + const toolbarActiveBackgroundColor = theme.getColor(toolbarActiveBackground); + if (toolbarActiveBackgroundColor) { + collector.addRule(` + .monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar:not(.vertical) .action-item.active { + background-color: ${toolbarActiveBackgroundColor}; + } + `); + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index e1e2505323..79104b4d84 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -25,12 +25,12 @@ import { Range } from 'vs/editor/common/core/range'; import { IEditor } from 'vs/editor/common/editorCommon'; import { IModeService } from 'vs/editor/common/services/modeService'; import * as nls from 'vs/nls'; -import { createActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IMenu, IMenuService, MenuId, MenuItemAction, MenuRegistry, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -42,8 +42,7 @@ import { IEditorMemento } from 'vs/workbench/common/editor'; import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { PANEL_BORDER } from 'vs/workbench/common/theme'; import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugColors'; -import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_OUTPUT_PADDING, CELL_RIGHT_MARGIN, CELL_RUN_GUTTER, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT, MARKDOWN_CELL_BOTTOM_MARGIN, MARKDOWN_CELL_TOP_MARGIN, MARKDOWN_PREVIEW_PADDING, SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CellEditState, CellFocusMode, IActiveNotebookEditor, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IGenericCellViewModel, IInsetRenderOutput, INotebookCellList, INotebookCellOutputLayoutInfo, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorMouseEvent, NotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_ID, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFocusMode, IActiveNotebookEditor, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IGenericCellViewModel, IInsetRenderOutput, INotebookCellList, INotebookCellOutputLayoutInfo, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorMouseEvent, INotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_ID, NOTEBOOK_OUTPUT_FOCUSED, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookDecorationCSSRules, NotebookRefCountedStyleSheet } from 'vs/workbench/contrib/notebook/browser/notebookEditorDecorations'; import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { NotebookEditorKernelManager } from 'vs/workbench/contrib/notebook/browser/notebookEditorKernelManager'; @@ -59,22 +58,20 @@ import { NotebookEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbenc import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; 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, ExperimentalUseMarkdownRenderer, SelectionStateType, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, ExperimentalUseMarkdownRenderer, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; 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 { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus'; -import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { isWeb } from 'vs/base/common/platform'; import { mark } from 'vs/workbench/contrib/notebook/common/notebookPerformance'; import { readFontInfo } from 'vs/editor/browser/config/configuration'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookEditorContextKeys } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetContextKeys'; +import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; +import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; +import { NotebookEditorToolbar } from 'vs/workbench/contrib/notebook/browser/notebookEditorToolbar'; +import { INotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; const $ = DOM.$; @@ -202,17 +199,20 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private static readonly EDITOR_MEMENTOS = new Map>(); private _overlayContainer!: HTMLElement; private _notebookTopToolbarContainer!: HTMLElement; + private _notebookTopToolbar!: NotebookEditorToolbar; private _body!: HTMLElement; + private _styleElement!: HTMLStyleElement; private _overflowContainer!: HTMLElement; private _webview: BackLayerWebView | null = null; private _webviewResolvePromise: Promise | null> | null = null; private _webviewTransparentCover: HTMLElement | null = null; + private _listDelegate: NotebookCellListDelegate | null = null; private _list!: INotebookCellList; private _listViewInfoAccessor!: ListViewInfoAccessor; private _dndController: CellDragAndDropController | null = null; private _listTopCellToolbar: ListTopCellToolbar | null = null; private _renderedEditors: Map = new Map(); - private _eventDispatcher: NotebookEventDispatcher | undefined; + private _viewContext: ViewContext; private _notebookViewModel: NotebookViewModel | undefined; private _localStore: DisposableStore = this._register(new DisposableStore()); private _localCellStateListeners: DisposableStore[] = []; @@ -305,15 +305,20 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor public readonly scopedContextKeyService: IContextKeyService; private readonly instantiationService: IInstantiationService; + private readonly _notebookOptions: NotebookOptions; + + get notebookOptions() { + return this._notebookOptions; + } constructor( readonly creationOptions: INotebookEditorCreationOptions, @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @IAccessibilityService accessibilityService: IAccessibilityService, + @INotebookRendererMessagingService private readonly notebookRendererMessaging: INotebookRendererMessagingService, @INotebookEditorService private readonly notebookEditorService: INotebookEditorService, - @INotebookKernelService notebookKernelService: INotebookKernelService, - @IEditorService private readonly editorService: IEditorService, + @INotebookKernelService private readonly notebookKernelService: INotebookKernelService, @IConfigurationService private readonly configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, @ILayoutService private readonly layoutService: ILayoutService, @@ -321,22 +326,23 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor @IMenuService private readonly menuService: IMenuService, @IThemeService private readonly themeService: IThemeService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IModeService private readonly modeService: IModeService, - @IKeybindingService private readonly keybindingService: IKeybindingService, - @optional(ITASExperimentService) private readonly experimentService: ITASExperimentService + @IModeService private readonly modeService: IModeService ) { super(); this.isEmbedded = creationOptions.isEmbedded || false; - this.useRenderer = !isWeb && !!this.configurationService.getValue(ExperimentalUseMarkdownRenderer) && !accessibilityService.isScreenReaderOptimized(); + this.useRenderer = !!this.configurationService.getValue(ExperimentalUseMarkdownRenderer) && !accessibilityService.isScreenReaderOptimized(); + this._notebookOptions = new NotebookOptions(this.configurationService); + this._register(this._notebookOptions); + this._viewContext = new ViewContext(this._notebookOptions, new NotebookEventDispatcher()); this._overlayContainer = document.createElement('div'); this.scopedContextKeyService = contextKeyService.createScoped(this._overlayContainer); this.instantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); - this._register(instantiationService.createInstance(NotebookEditorContextKeys, this)); + this._register(this.instantiationService.createInstance(NotebookEditorContextKeys, this)); - this._kernelManger = instantiationService.createInstance(NotebookEditorKernelManager); + this._kernelManger = this.instantiationService.createInstance(NotebookEditorKernelManager); this._register(notebookKernelService.onDidChangeNotebookKernelBinding(e => { if (isEqual(e.notebook, this.viewModel?.uri)) { this._loadKernelPreloads(); @@ -345,21 +351,33 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._memento = new Memento(NOTEBOOK_EDITOR_ID, storageService); - this._outputRenderer = new OutputRenderer(this, this.instantiationService); + this._outputRenderer = this._register(new OutputRenderer(this, this.instantiationService)); this._scrollBeyondLastLine = this.configurationService.getValue('editor.scrollBeyondLastLine'); - this.configurationService.onDidChangeConfiguration(e => { + this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('editor.scrollBeyondLastLine')) { this._scrollBeyondLastLine = this.configurationService.getValue('editor.scrollBeyondLastLine'); if (this._dimension && this._isVisible) { this.layout(this._dimension); } } + })); - if (e.affectsConfiguration(CellToolbarLocKey) || e.affectsConfiguration(ShowCellStatusBarKey)) { + this._register(this._notebookOptions.onDidChangeOptions(e => { + if (e.cellStatusBarVisibility || e.cellToolbarLocation || e.cellToolbarInteraction) { this._updateForNotebookConfiguration(); } - }); + + if (e.compactView || e.focusIndicator || e.insertToolbarPosition || e.cellToolbarLocation || e.dragAndDropEnabled || e.fontSize || e.insertToolbarAlignment) { + this._styleElement?.remove(); + this._createLayoutStyles(); + this._webview?.updateOptions(this.notebookOptions.computeWebviewOptions()); + } + + if (this._dimension && this._isVisible) { + this.layout(this._dimension); + } + })); this.notebookEditorService.addNotebookEditor(this); @@ -384,12 +402,20 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor contributions = NotebookEditorExtensionsRegistry.getEditorContributions(); } for (const desc of contributions) { + let contribution: INotebookEditorContribution | undefined; try { - const contribution = this.instantiationService.createInstance(desc.ctor, this); - this._contributions.set(desc.id, contribution); + contribution = this.instantiationService.createInstance(desc.ctor, this); } catch (err) { onUnexpectedError(err); } + if (contribution) { + if (!this._contributions.has(desc.id)) { + this._contributions.set(desc.id, contribution); + } else { + contribution.dispose(); + throw new Error(`DUPLICATE notebook editor contribution: '${desc.id}'`); + } + } } this._updateForNotebookConfiguration(); @@ -479,41 +505,22 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } - const cellToolbarLocation = this.configurationService.getValue(CellToolbarLocKey); this._overlayContainer.classList.remove('cell-title-toolbar-left'); this._overlayContainer.classList.remove('cell-title-toolbar-right'); this._overlayContainer.classList.remove('cell-title-toolbar-hidden'); + const cellToolbarLocation = this._notebookOptions.computeCellToolbarLocation(this.viewModel?.viewType); + this._overlayContainer.classList.add(`cell-title-toolbar-${cellToolbarLocation}`); - if (typeof cellToolbarLocation === 'string') { - if (cellToolbarLocation === 'left' || cellToolbarLocation === 'right' || cellToolbarLocation === 'hidden') { - this._overlayContainer.classList.add(`cell-title-toolbar-${cellToolbarLocation}`); - } - } else { - if (this.viewModel) { - const notebookSpecificSetting = cellToolbarLocation[this.viewModel.viewType] ?? cellToolbarLocation['default']; - let cellToolbarLocationForCurrentView = 'right'; + const cellToolbarInteraction = this._notebookOptions.getLayoutConfiguration().cellToolbarInteraction; + let cellToolbarInteractionState = 'hover'; + this._overlayContainer.classList.remove('cell-toolbar-hover'); + this._overlayContainer.classList.remove('cell-toolbar-click'); - switch (notebookSpecificSetting) { - case 'left': - cellToolbarLocationForCurrentView = 'left'; - break; - case 'right': - cellToolbarLocationForCurrentView = 'right'; - case 'hidden': - cellToolbarLocationForCurrentView = 'hidden'; - default: - cellToolbarLocationForCurrentView = 'right'; - break; - } - - this._overlayContainer.classList.add(`cell-title-toolbar-${cellToolbarLocationForCurrentView}`); - } else { - this._overlayContainer.classList.add(`cell-title-toolbar-right`); - } + if (cellToolbarInteraction === 'hover' || cellToolbarInteraction === 'click') { + cellToolbarInteractionState = cellToolbarInteraction; } + this._overlayContainer.classList.add(`cell-toolbar-${cellToolbarInteractionState}`); - const showCellStatusBar = this.configurationService.getValue(ShowCellStatusBarKey); - this._overlayContainer.classList.toggle('cell-statusbar-hidden', !showCellStatusBar); } private _generateFontInfo(): void { @@ -523,19 +530,248 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private _createBody(parent: HTMLElement): void { this._notebookTopToolbarContainer = document.createElement('div'); - this._notebookTopToolbarContainer.classList.add('notebook-top-toolbar'); + this._notebookTopToolbarContainer.classList.add('notebook-toolbar-container'); this._notebookTopToolbarContainer.style.display = 'none'; DOM.append(parent, this._notebookTopToolbarContainer); this._body = document.createElement('div'); - this._body.classList.add('cell-list-container'); - this._createCellList(); DOM.append(parent, this._body); + this._body.classList.add('cell-list-container'); + this._createLayoutStyles(); + this._createCellList(); this._overflowContainer = document.createElement('div'); this._overflowContainer.classList.add('notebook-overflow-widget-container', 'monaco-editor'); DOM.append(parent, this._overflowContainer); } + private _createLayoutStyles(): void { + this._styleElement = DOM.createStyleSheet(this._body); + const { + cellRightMargin, + cellTopMargin, + cellRunGutter, + cellBottomMargin, + codeCellLeftMargin, + markdownCellGutter, + markdownCellLeftMargin, + markdownCellBottomMargin, + markdownCellTopMargin, + // bottomToolbarGap: bottomCellToolbarGap, + // bottomToolbarHeight: bottomCellToolbarHeight, + collapsedIndicatorHeight, + compactView, + focusIndicator, + insertToolbarPosition, + insertToolbarAlignment, + fontSize, + focusIndicatorLeftMargin + } = this._notebookOptions.getLayoutConfiguration(); + + const { bottomToolbarGap, bottomToolbarHeight } = this._notebookOptions.computeBottomToolbarDimensions(this.viewModel?.viewType); + + const styleSheets: string[] = []; + + styleSheets.push(` + :root { + --notebook-cell-output-font-size: ${fontSize}px; + } + `); + + if (compactView) { + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row div.cell.code { margin-left: ${codeCellLeftMargin + cellRunGutter}px; }`); + } else { + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row div.cell.code { margin-left: ${codeCellLeftMargin}px; }`); + } + + // focus indicator + if (focusIndicator === 'border') { + styleSheets.push(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom:before, + .monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row .cell-inner-container:before, + .monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row .cell-inner-container:after { + content: ""; + position: absolute; + width: 100%; + height: 1px; + } + + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { + content: ""; + position: absolute; + width: 1px; + height: 100%; + z-index: 10; + } + + /* top border */ + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before { + border-top: 1px solid transparent; + } + + /* left border */ + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before { + border-left: 1px solid transparent; + } + + /* bottom border */ + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom:before { + border-bottom: 1px solid transparent; + } + + /* right border */ + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { + border-right: 1px solid transparent; + } + `); + + // left and right border margins + styleSheets.push(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-right:before, + .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-right:before { + top: -${cellTopMargin}px; height: calc(100% + ${cellTopMargin + cellBottomMargin}px) + }`); + } else { + // gutter + styleSheets.push(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { + content: ""; + position: absolute; + width: 0px; + height: 100%; + z-index: 10; + } + `); + + // left and right border margins + styleSheets.push(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-right:before, + .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-right:before { + top: 0px; height: 100%; + }`); + + styleSheets.push(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.selected .cell-focus-indicator-left:before { + border-left: 3px solid transparent; + border-radius: 2px; + margin-left: ${focusIndicatorLeftMargin}px; + }`); + + // boder should always show + styleSheets.push(` + .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container .cell-focus-indicator-left:before { + border-color: var(--notebook-focused-cell-border-color) !important; + } + + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-inner-container .cell-focus-indicator-left:before { + border-color: var(--notebook-inactive-focused-cell-border-color) !important; + } + `); + } + + // between cell insert toolbar + if (insertToolbarPosition === 'betweenCells' || insertToolbarPosition === 'both') { + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { display: flex; }`); + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .cell-list-top-cell-toolbar-container { display: flex; }`); + } else { + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { display: none; }`); + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .cell-list-top-cell-toolbar-container { display: none; }`); + } + + if (insertToolbarAlignment === 'left') { + styleSheets.push(` + .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .action-item:first-child, + .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .action-item:first-child, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .action-item:first-child { + margin-right: 0px !important; + }`); + + styleSheets.push(` + .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar .action-label, + .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar .action-label, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar .action-label { + padding: 0px !important; + justify-content: center; + border-radius: 4px; + }`); + + styleSheets.push(` + .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container, + .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { + align-items: flex-start; + justify-content: left; + margin: 0 16px 0 ${8 + codeCellLeftMargin}px; + }`); + + styleSheets.push(` + .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container, + .notebookOverlay .cell-bottom-toolbar-container .action-item { + border: 0px; + }`); + } + + // top insert toolbar + const topInsertToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(this.viewModel?.viewType); + styleSheets.push(`.notebookOverlay .cell-list-top-cell-toolbar-container { top: -${topInsertToolbarHeight}px }`); + styleSheets.push(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element, + .notebookOverlay > .cell-list-container > .notebook-gutter > .monaco-list > .monaco-scrollable-element { + padding-top: ${topInsertToolbarHeight}px; + box-sizing: border-box; + }`); + + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .code-cell-row div.cell.code { margin-left: ${codeCellLeftMargin + cellRunGutter}px; }`); + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell { margin-right: ${cellRightMargin}px; }`); + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .cell-inner-container { padding-top: ${cellTopMargin}px; }`); + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row > .cell-inner-container { padding-bottom: ${markdownCellBottomMargin}px; padding-top: ${markdownCellTopMargin}px; }`); + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row > .cell-inner-container.webview-backed-markdown-cell { padding: 0; }`); + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row > .webview-backed-markdown-cell.markdown-cell-edit-mode .cell.code { padding-bottom: ${markdownCellBottomMargin}px; padding-top: ${markdownCellTopMargin}px; }`); + styleSheets.push(`.notebookOverlay .output { margin: 0px ${cellRightMargin}px 0px ${codeCellLeftMargin + cellRunGutter}px; }`); + styleSheets.push(`.notebookOverlay .output { width: calc(100% - ${codeCellLeftMargin + cellRunGutter + cellRightMargin}px); }`); + + // output toolbar + styleSheets.push(`.monaco-workbench .notebookOverlay .output .cell-output-toolbar { left: -${cellRunGutter}px; }`); + styleSheets.push(`.monaco-workbench .notebookOverlay .output .cell-output-toolbar { width: ${cellRunGutter}px; }`); + + styleSheets.push(`.notebookOverlay .output-show-more-container { margin: 0px ${cellRightMargin}px 0px ${codeCellLeftMargin + cellRunGutter}px; }`); + styleSheets.push(`.notebookOverlay .output-show-more-container { width: calc(100% - ${codeCellLeftMargin + cellRunGutter + cellRightMargin}px); }`); + styleSheets.push(`.notebookOverlay .cell .run-button-container { width: ${cellRunGutter}px; left: ${codeCellLeftMargin}px }`); + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .execution-count-label { left: ${codeCellLeftMargin}px; width: ${cellRunGutter}px; }`); + + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell.markdown { padding-left: ${cellRunGutter}px; }`); + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container .notebook-folding-indicator { left: ${(markdownCellGutter - 20) / 2 + markdownCellLeftMargin}px; }`); + styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row :not(.webview-backed-markdown-cell) .cell-focus-indicator-top { height: ${cellTopMargin}px; }`); + styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-side { bottom: ${bottomToolbarGap}px; }`); + styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row.code-cell-row .cell-focus-indicator-left, + .notebookOverlay .monaco-list .monaco-list-row.code-cell-row .cell-drag-handle { width: ${codeCellLeftMargin + cellRunGutter}px; }`); + styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .cell-focus-indicator-left { width: ${codeCellLeftMargin}px; }`); + styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator.cell-focus-indicator-right { width: ${cellRightMargin}px; }`); + styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom { height: ${cellBottomMargin}px; }`); + styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row .cell-shadow-container-bottom { top: ${cellBottomMargin}px; }`); + + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-collapsed-part { margin-left: ${codeCellLeftMargin + cellRunGutter}px; height: ${collapsedIndicatorHeight}px; }`); + + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { height: ${bottomToolbarHeight}px }`); + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .cell-list-top-cell-toolbar-container { height: ${bottomToolbarHeight}px }`); + + // cell toolbar + styleSheets.push(`.monaco-workbench .notebookOverlay.cell-title-toolbar-right > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { + right: ${cellRightMargin + 26}px; + } + .monaco-workbench .notebookOverlay.cell-title-toolbar-left > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { + left: ${codeCellLeftMargin + cellRunGutter + 16}px; + } + .monaco-workbench .notebookOverlay.cell-title-toolbar-hidden > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { + display: none; + }`); + + this._styleElement.textContent = styleSheets.join('\n'); + } + private _createCellList(): void { this._body.classList.add('cell-list-container'); @@ -546,12 +782,20 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this.instantiationService.createInstance(MarkdownCellRenderer, this, this._dndController, this._renderedEditors, getScopedContextKeyService, { useRenderer: this.useRenderer }), ]; + renderers.forEach(renderer => { + this._register(renderer); + }); + + this._listDelegate = this.instantiationService.createInstance(NotebookCellListDelegate); + this._register(this._listDelegate); + this._list = this.instantiationService.createInstance( NotebookCellList, 'NotebookCellList', this._overlayContainer, this._body, - this.instantiationService.createInstance(NotebookCellListDelegate), + this._viewContext, + this._listDelegate, renderers, this.scopedContextKeyService, { @@ -592,7 +836,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const index = this.viewModel.getCellIndex(element); if (index >= 0) { - return `Cell ${index}, ${element.cellKind === CellKind.Markdown ? 'markdown' : 'code'} cell`; + return `Cell ${index}, ${element.cellKind === CellKind.Markup ? 'markdown' : 'code'} cell`; } return ''; @@ -673,19 +917,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._register(widgetFocusTracker.onDidBlur(() => this._onDidBlurEmitter.fire())); this._reigsterNotebookActionsToolbar(); - this._register(this.editorService.onDidActiveEditorChange(() => { - if (this.editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) { - const notebookEditor = this.editorService.activeEditorPane.getControl() as INotebookEditor; - if (notebookEditor === this) { - // this is the active editor - this._showNotebookActionsinEditorToolbar(); - return; - } - } - this._editorToolbarDisposable.clear(); - this._toolbarActionDisposable.clear(); - })); } private showListContextMenu(e: IListContextMenuEvent) { @@ -701,126 +933,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor }); } - private _notebookGlobalActionsMenu!: IMenu; - private _toolbarActionDisposable = this._register(new DisposableStore()); - private _topToolbar!: ToolBar; - private _useGlobalToolbar: boolean = false; - private _editorToolbarDisposable = this._register(new DisposableStore()); private _reigsterNotebookActionsToolbar() { - const cellMenu = this.instantiationService.createInstance(CellMenus); - this._notebookGlobalActionsMenu = this._register(cellMenu.getNotebookToolbar(this.scopedContextKeyService)); - this._register(this._notebookGlobalActionsMenu); - - this._useGlobalToolbar = this.configurationService.getValue('notebook.experimental.globalToolbar') ?? false; - this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('notebook.experimental.globalToolbar')) { - this._useGlobalToolbar = this.configurationService.getValue('notebook.experimental.globalToolbar'); - this._showNotebookActionsinEditorToolbar(); + this._notebookTopToolbar = this._register(this.instantiationService.createInstance(NotebookEditorToolbar, this, this.scopedContextKeyService, this._notebookTopToolbarContainer)); + this._register(this._notebookTopToolbar.onDidChangeState(() => { + if (this._dimension && this._isVisible) { + this.layout(this._dimension); } })); - - this._topToolbar = new ToolBar(this._notebookTopToolbarContainer, this.contextMenuService, { - getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), - actionViewItemProvider: action => { - return createActionViewItem(this.instantiationService, action); - }, - renderDropdownAsChildElement: true - }); - this._register(this._topToolbar); - this._topToolbar.context = { - ui: true, - notebookEditor: this - }; - - this._showNotebookActionsinEditorToolbar(); - this._register(this._notebookGlobalActionsMenu.onDidChange(() => { - this._showNotebookActionsinEditorToolbar(); - })); - - if (this.experimentService) { - this.experimentService.getTreatment('nbtoolbarineditor').then(treatment => { - if (treatment === undefined) { - return; - } - if (this._useGlobalToolbar !== treatment) { - this._useGlobalToolbar = treatment; - this._showNotebookActionsinEditorToolbar(); - } - }); - } - } - - private _showNotebookActionsinEditorToolbar() { - // when there is no view model, just ignore. - if (!this.viewModel) { - return; - } - - if (!this._useGlobalToolbar) { - // schedule actions registration in next frame, otherwise we are seeing duplicated notbebook actions temporarily - this._editorToolbarDisposable.clear(); - this._editorToolbarDisposable.add(DOM.scheduleAtNextAnimationFrame(() => { - const groups = this._notebookGlobalActionsMenu.getActions({ shouldForwardArgs: true }); - this._toolbarActionDisposable.clear(); - this._topToolbar.setActions([], []); - if (!this.viewModel) { - return; - } - - if (!this._isVisible) { - return; - } - - if (this.editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) { - const notebookEditor = this.editorService.activeEditorPane.getControl() as INotebookEditor; - if (notebookEditor !== this) { - // clear actions but not recreate because it is not active editor - return; - } - } - - groups.forEach(group => { - const groupName = group[0]; - const actions = group[1]; - - let order = groupName === 'navigation' ? -10 : 0; - for (let i = 0; i < actions.length; i++) { - const menuItemAction = actions[i] as MenuItemAction; - this._toolbarActionDisposable.add(MenuRegistry.appendMenuItem(MenuId.EditorTitle, { - command: { - id: menuItemAction.item.id, - title: menuItemAction.item.title, - category: menuItemAction.item.category, - tooltip: menuItemAction.item.tooltip, - icon: menuItemAction.item.icon, - precondition: menuItemAction.item.precondition, - toggled: menuItemAction.item.toggled, - }, - title: menuItemAction.item.title + ' ' + this.viewModel?.uri.scheme, - group: groupName, - order: order - })); - order++; - } - }); - })); - - this._notebookTopToolbarContainer.style.display = 'none'; - } else { - this._toolbarActionDisposable.clear(); - this._topToolbar.setActions([], []); - const groups = this._notebookGlobalActionsMenu.getActions({ shouldForwardArgs: true }); - this._notebookTopToolbarContainer.style.display = 'flex'; - const primaryGroup = groups.find(group => group[0] === 'navigation'); - const primaryActions = primaryGroup ? primaryGroup[1] : []; - const secondaryActions = groups.filter(group => group[0] !== 'navigation').reduce((prev: (MenuItemAction | SubmenuItemAction)[], curr) => { prev.push(...curr[1]); return prev; }, []); - - this._topToolbar.setActions(primaryActions, secondaryActions); - } - - if (this._dimension && this._isVisible) { - this.layout(this._dimension); - } } private _updateForCursorNavigationMode(applyFocusChange: () => void): void { @@ -858,9 +977,20 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor async setModel(textModel: NotebookTextModel, viewState: INotebookEditorViewState | undefined): Promise { if (this.viewModel === undefined || !this.viewModel.equal(textModel)) { + const oldTopInsertToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(this.viewModel?.viewType); + const oldBottomToolbarDimensions = this._notebookOptions.computeBottomToolbarDimensions(this.viewModel?.viewType); this._detachModel(); await this._attachModel(textModel, viewState); + const newTopInsertToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(this.viewModel?.viewType); + const newBottomToolbarDimensions = this._notebookOptions.computeBottomToolbarDimensions(this.viewModel?.viewType); + if (oldTopInsertToolbarHeight !== newTopInsertToolbarHeight + || oldBottomToolbarDimensions.bottomToolbarGap !== newBottomToolbarDimensions.bottomToolbarGap + || oldBottomToolbarDimensions.bottomToolbarHeight !== newBottomToolbarDimensions.bottomToolbarHeight) { + this._styleElement?.remove(); + this._createLayoutStyles(); + this._webview?.updateOptions(this.notebookOptions.computeWebviewOptions()); + } type WorkbenchNotebookOpenClassification = { scheme: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; ext: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; @@ -910,7 +1040,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - async setOptions(options: NotebookEditorOptions | undefined) { + async setOptions(options: INotebookEditorOptions | undefined) { if (!this.hasModel()) { return; } @@ -925,7 +1055,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const cell = this.viewModel.viewCells.find(cell => cell.uri.toString() === cellOptions.resource.toString()); if (cell) { this.focusElement(cell); - await this.revealInCenterIfOutsideViewportAsync(cell); + const selection = cellOptions.options?.selection; + if (selection) { + await this.revealLineInCenterIfOutsideViewportAsync(cell, selection.startLineNumber); + } else { + await this.revealInCenterIfOutsideViewportAsync(cell); + } + const editor = this._renderedEditors.get(cell)!; if (editor) { if (cellOptions.options?.selection) { @@ -1043,14 +1179,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } private async _createWebview(id: string, resource: URI): Promise { - this._webview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, { - outputNodePadding: CELL_OUTPUT_PADDING, - outputNodeLeftPadding: CELL_OUTPUT_PADDING, - previewNodePadding: MARKDOWN_PREVIEW_PADDING, - leftMargin: CODE_CELL_LEFT_MARGIN, - rightMargin: CELL_RIGHT_MARGIN, - runGutter: CELL_RUN_GUTTER, - }); + this._webview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, this._notebookOptions.computeWebviewOptions(), this.notebookRendererMessaging.getScoped(this._uuid)); this._webview.element.style.width = '100%'; // attach the webview container to the DOM tree first @@ -1059,10 +1188,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private async _attachModel(textModel: NotebookTextModel, viewState: INotebookEditorViewState | undefined) { await this._createWebview(this.getId(), textModel.uri); - - this._eventDispatcher = new NotebookEventDispatcher(); - this.viewModel = this.instantiationService.createInstance(NotebookViewModel, textModel.viewType, textModel, this._eventDispatcher, this.getLayoutInfo()); - this._eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); + this.viewModel = this.instantiationService.createInstance(NotebookViewModel, textModel.viewType, textModel, this._viewContext, this.getLayoutInfo()); + this._viewContext.eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); this._updateForOptions(); this._updateForNotebookConfiguration(); @@ -1118,7 +1245,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const deletedCells: MarkdownCellViewModel[] = []; for (const cell of cells) { - if (cell.cellKind === CellKind.Markdown) { + if (cell.cellKind === CellKind.Markup) { const mdCell = cell as MarkdownCellViewModel; if (this.viewModel?.viewCells.find(cell => cell.handle === mdCell.handle)) { // Cell has been folded but is still in model @@ -1161,7 +1288,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor })); if (this._dimension) { - this._list.layout(this._dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP, this._dimension.width); + const topInserToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(this.viewModel?.viewType); + this._list.layout(this._dimension.height - topInserToolbarHeight, this._dimension.width); } else { this._list.layout(); } @@ -1191,7 +1319,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor })); } - if (cell.cellKind === CellKind.Markdown) { + if (cell.cellKind === CellKind.Markup) { store.add((cell as MarkdownCellViewModel).onDidHideInput(() => { this.hideMarkdownPreviews([(cell as MarkdownCellViewModel)]); })); @@ -1245,7 +1373,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor offset += (totalHeightCache ? totalHeightCache[i] : 0); continue; } else { - if (cell.cellKind === CellKind.Markdown) { + if (cell.cellKind === CellKind.Markup) { requests.push([cell, offset]); } } @@ -1257,19 +1385,27 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - await this._webview!.initializeMarkdown(requests - .map(request => ({ cellId: request[0].id, cellHandle: request[0].handle, content: request[0].getText(), offset: request[1] }))); + await this._webview!.initializeMarkup(requests.map(request => ({ + mime: 'text/markdown', + cellId: request[0].id, + cellHandle: request[0].handle, + content: request[0].getText(), + offset: request[1], + visible: false, + }))); } else { - const initRequests = viewModel.viewCells.filter(cell => cell.cellKind === CellKind.Markdown).slice(0, 5).map(cell => ({ cellId: cell.id, cellHandle: cell.handle, content: cell.getText(), offset: -10000 })); - await this._webview!.initializeMarkdown(initRequests); + const initRequests = viewModel.viewCells.filter(cell => cell.cellKind === CellKind.Markup).slice(0, 5).map(cell => ({ + cellId: cell.id, cellHandle: cell.handle, content: cell.getText(), offset: -10000, visible: false, mime: 'text/markdown', + })); + await this._webview!.initializeMarkup(initRequests); // no cached view state so we are rendering the first viewport // after above async call, we already get init height for markdown cells, we can update their offset let offset = 0; - const offsetUpdateRequests: { id: string, top: number }[] = []; + const offsetUpdateRequests: { id: string, top: number; }[] = []; const scrollBottom = Math.max(this._dimension?.height ?? 0, 1080); for (const cell of viewModel.viewCells) { - if (cell.cellKind === CellKind.Markdown) { + if (cell.cellKind === CellKind.Markup) { offsetUpdateRequests.push({ id: cell.id, top: offset }); } @@ -1330,7 +1466,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor if (this._list) { state.scrollPosition = { left: this._list.scrollLeft, top: this._list.scrollTop }; - const cellHeights: { [key: number]: number } = {}; + const cellHeights: { [key: number]: number; } = {}; for (let i = 0; i < this.viewModel!.length; i++) { const elm = this.viewModel!.cellAt(i) as CellViewModel; if (elm.cellKind === CellKind.Code) { @@ -1356,7 +1492,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } // Save contribution view states - const contributionsState: { [key: string]: unknown } = {}; + const contributionsState: { [key: string]: unknown; } = {}; for (const [id, contribution] of this._contributions) { if (typeof contribution.saveViewState === 'function') { contributionsState[id] = contribution.saveViewState(); @@ -1384,16 +1520,18 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor }; } + const topInserToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(this.viewModel?.viewType); + this._dimension = new DOM.Dimension(dimension.width, dimension.height); - DOM.size(this._body, dimension.width, dimension.height - (this._useGlobalToolbar ? /** Toolbar height */ 26 : 0)); - if (this._list.getRenderHeight() < dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP) { + DOM.size(this._body, dimension.width, dimension.height - (this._notebookTopToolbar?.useGlobalToolbar ? /** Toolbar height */ 26 : 0)); + if (this._list.getRenderHeight() < dimension.height - topInserToolbarHeight) { // the new dimension is larger than the list viewport, update its additional height first, otherwise the list view will move down a bit (as the `scrollBottom` will move down) - this._list.updateOptions({ additionalScrollHeight: this._scrollBeyondLastLine ? Math.max(0, (dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP - 50)) : SCROLLABLE_ELEMENT_PADDING_TOP }); - this._list.layout(dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP, dimension.width); + this._list.updateOptions({ additionalScrollHeight: this._scrollBeyondLastLine ? Math.max(0, (dimension.height - topInserToolbarHeight - 50)) : topInserToolbarHeight }); + this._list.layout(dimension.height - topInserToolbarHeight, dimension.width); } else { // the new dimension is smaller than the list viewport, if we update the additional height, the `scrollBottom` will move up, which moves the whole list view upwards a bit. So we run a layout first. - this._list.layout(dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP, dimension.width); - this._list.updateOptions({ additionalScrollHeight: this._scrollBeyondLastLine ? Math.max(0, (dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP - 50)) : SCROLLABLE_ELEMENT_PADDING_TOP }); + this._list.layout(dimension.height - topInserToolbarHeight, dimension.width); + this._list.updateOptions({ additionalScrollHeight: this._scrollBeyondLastLine ? Math.max(0, (dimension.height - topInserToolbarHeight - 50)) : topInserToolbarHeight }); } this._overlayContainer.style.visibility = 'visible'; @@ -1411,7 +1549,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._webviewTransparentCover.style.width = `${dimension.width}px`; } - this._eventDispatcher?.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); + this._notebookTopToolbar.layout(); + + this._viewContext?.eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); } //#endregion @@ -1454,11 +1594,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const focused = DOM.isAncestor(document.activeElement, this._overlayContainer); this._editorFocus.set(focused); this.viewModel?.setFocus(focused); - - if (!focused) { - this._editorToolbarDisposable.clear(); - this._toolbarActionDisposable.clear(); - } } hasFocus() { @@ -1686,21 +1821,15 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor //#region Kernel/Execution - private async _loadKernelPreloads() { - const kernel = this.activeKernel; - if (!kernel) { + private async _loadKernelPreloads(): Promise { + if (!this.hasModel()) { return; } - const preloadUris = kernel.preloadUris; - if (!preloadUris.length) { - return; - } - + const { selected } = this.notebookKernelService.getMatchingKernel(this.viewModel.notebookDocument); if (!this._webview?.isResolved()) { await this._resolveWebview(); } - - this._webview?.updateKernelPreloads([kernel.localResourceRoot], kernel.preloadUris); + this._webview?.updateKernelPreloads(selected); } get activeKernel() { @@ -1730,7 +1859,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor //#endregion //#region Cell operations/layout API - private _pendingLayouts = new WeakMap(); + private _pendingLayouts: WeakMap | null = new WeakMap(); async layoutNotebookCell(cell: ICellViewModel, height: number): Promise { this._debug('layout cell', cell.handle, height); const viewIndex = this._list.getViewIndex(cell); @@ -1747,8 +1876,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._list.updateElementHeight2(cell, height); }; - if (this._pendingLayouts.has(cell)) { - this._pendingLayouts.get(cell)!.dispose(); + if (this._pendingLayouts?.has(cell)) { + this._pendingLayouts?.get(cell)!.dispose(); } let r: () => void; @@ -1761,13 +1890,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } - this._pendingLayouts.delete(cell); + this._pendingLayouts?.delete(cell); relayout(cell, height); r(); }); - this._pendingLayouts.set(cell, toDisposable(() => { + this._pendingLayouts?.set(cell, toDisposable(() => { layoutDisposable.dispose(); r(); })); @@ -1800,7 +1929,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const defaultLanguage = supportedLanguages[0] || 'plaintext'; if (cell?.cellKind === CellKind.Code) { language = cell.language; - } else if (cell?.cellKind === CellKind.Markdown) { + } else if (cell?.cellKind === CellKind.Markup) { const nearestCodeCellIndex = this._nearestCodeCellIndex(index); if (nearestCodeCellIndex > -1) { language = this.viewModel.cellAt(nearestCodeCellIndex)!.language; @@ -1855,7 +1984,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return false; } - if (this._pendingLayouts.has(cell)) { + if (this._pendingLayouts?.has(cell)) { this._pendingLayouts.get(cell)!.dispose(); } @@ -1978,13 +2107,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor let position = ''; switch (focusItem) { case 'editor': - position = `the inner ${cell.cellKind === CellKind.Markdown ? 'markdown' : 'code'} editor is focused, press escape to focus the cell container`; + position = `the inner ${cell.cellKind === CellKind.Markup ? 'markdown' : 'code'} editor is focused, press escape to focus the cell container`; break; case 'output': position = `the cell output is focused, press escape to focus the cell container`; break; case 'container': - position = `the ${cell.cellKind === CellKind.Markdown ? 'markdown preview' : 'cell container'} is focused, press enter to focus the inner ${cell.cellKind === CellKind.Markdown ? 'markdown' : 'code'} editor`; + position = `the ${cell.cellKind === CellKind.Markup ? 'markdown preview' : 'cell container'} is focused, press enter to focus the inner ${cell.cellKind === CellKind.Markup ? 'markdown' : 'code'} editor`; break; default: break; @@ -1993,19 +2122,38 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - toggleNotebookCellSelection(cell: ICellViewModel): void { + toggleNotebookCellSelection(selectedCell: ICellViewModel, selectFromPrevious: boolean): void { const currentSelections = this._list.getSelectedElements(); + const isSelected = currentSelections.includes(selectedCell); - const isSelected = currentSelections.includes(cell); + const previousSelection = selectFromPrevious ? currentSelections[currentSelections.length - 1] ?? selectedCell : selectedCell; + const selectedIndex = this._list.getViewIndex(selectedCell)!; + const previousIndex = this._list.getViewIndex(previousSelection)!; + + const cellsInSelectionRange = this.getCellsInRange(selectedIndex, previousIndex); if (isSelected) { // Deselect - this._list.selectElements(currentSelections.filter(current => current !== cell)); + this._list.selectElements(currentSelections.filter(current => !cellsInSelectionRange.includes(current))); } else { // Add to selection - this._list.selectElements([...currentSelections, cell]); + this.focusElement(selectedCell); + this._list.selectElements([...currentSelections.filter(current => !cellsInSelectionRange.includes(current)), ...cellsInSelectionRange]); } } + private getCellsInRange(fromInclusive: number, toInclusive: number): ICellViewModel[] { + const selectedCellsInRange: ICellViewModel[] = []; + for (let index = 0; index < this._list.length; ++index) { + const cell = this._list.element(index); + if (cell) { + if ((index >= fromInclusive && index <= toInclusive) || (index >= toInclusive && index <= fromInclusive)) { + selectedCellsInRange.push(cell); + } + } + } + return selectedCellsInRange; + } + focusNotebookCell(cell: ICellViewModel, focusItem: 'editor' | 'container' | 'output', options?: IFocusNotebookCellOptions) { if (this._isDisposed) { return; @@ -2115,7 +2263,14 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } const cellTop = this._list.getAbsoluteTopOfElement(cell); - await this._webview.showMarkdownPreview(cell.id, cell.handle, cell.getText(), cellTop, cell.contentHash); + await this._webview.showMarkdownPreview({ + mime: 'text/markdown', + cellHandle: cell.handle, + cellId: cell.id, + content: cell.getText(), + offset: cellTop, + visible: true, + }); } async unhideMarkdownPreviews(cells: readonly MarkdownCellViewModel[]) { @@ -2198,6 +2353,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } + if (output.type === RenderOutputType.Extension) { + this.notebookRendererMessaging.prepare(output.renderer.id); + } + const cellTop = this._list.getAbsoluteTopOfElement(cell); if (!this._webview.insetMapping.has(output.source)) { await this._webview.createOutput({ cellId: cell.id, cellHandle: cell.handle, cellUri: cell.uri }, output, cellTop, offset); @@ -2241,13 +2400,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor readonly onDidReceiveMessage: Event = this._onDidReceiveMessage.event; - postMessage(forRendererId: string | undefined, message: any) { + postMessage(message: any) { if (this._webview?.isResolved()) { - if (forRendererId === undefined) { - this._webview.webview.postMessage(message); - } else { - this._webview.postRendererMessage(forRendererId, message); - } + this._webview.postKernelMessage(message); } } @@ -2325,7 +2480,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._webview.removeInsets(removedItems); - const markdownUpdateItems: { id: string, top: number }[] = []; + const markdownUpdateItems: { id: string, top: number; }[] = []; for (const cellId of this._webview.markdownPreviewMapping.keys()) { const cell = this.viewModel?.viewCells.find(cell => cell.id === cellId); if (cell) { @@ -2352,10 +2507,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor updateMarkdownCellHeight(cellId: string, height: number, isInit: boolean) { const cell = this.getCellById(cellId); if (cell && cell instanceof MarkdownCellViewModel) { - if (height + BOTTOM_CELL_TOOLBAR_GAP !== cell.layoutInfo.totalHeight) { - this._debug('updateMarkdownCellHeight', cell.handle, height + BOTTOM_CELL_TOOLBAR_GAP, isInit); - cell.renderedMarkdownHeight = height; - } + const { bottomToolbarGap } = this._notebookOptions.computeBottomToolbarDimensions(this.viewModel?.viewType); + this._debug('updateMarkdownCellHeight', cell.handle, height + bottomToolbarGap, isInit); + cell.renderedMarkdownHeight = height; } } @@ -2366,24 +2520,24 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - markdownCellDragStart(cellId: string, ctx: { clientY: number }): void { + markdownCellDragStart(cellId: string, event: { dragOffsetY: number; }): void { const cell = this.getCellById(cellId); if (cell instanceof MarkdownCellViewModel) { - this._dndController?.startExplicitDrag(cell, ctx); + this._dndController?.startExplicitDrag(cell, event.dragOffsetY); } } - markdownCellDrag(cellId: string, ctx: { clientY: number }): void { + markdownCellDrag(cellId: string, event: { dragOffsetY: number; }): void { const cell = this.getCellById(cellId); if (cell instanceof MarkdownCellViewModel) { - this._dndController?.explicitDrag(cell, ctx); + this._dndController?.explicitDrag(cell, event.dragOffsetY); } } - markdownCellDrop(cellId: string, ctx: { clientY: number, ctrlKey: boolean, altKey: boolean }): void { + markdownCellDrop(cellId: string, event: { dragOffsetY: number, ctrlKey: boolean, altKey: boolean; }): void { const cell = this.getCellById(cellId); if (cell instanceof MarkdownCellViewModel) { - this._dndController?.explicitDrop(cell, ctx); + this._dndController?.explicitDrop(cell, event); } } @@ -2420,10 +2574,23 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._overlayContainer.remove(); this.viewModel?.dispose(); + + // unref + this._webview = null; + this._webviewResolvePromise = null; + this._webviewTransparentCover = null; + this._dndController = null; + this._listTopCellToolbar = null; + this._notebookViewModel = undefined; + this._cellContextKeyManager = null; + this._renderedEditors.clear(); + this._pendingLayouts = null; + this._listDelegate = null; + super.dispose(); } - toJSON(): { notebookUri: URI | undefined } { + toJSON(): { notebookUri: URI | undefined; } { return { notebookUri: this.viewModel?.uri, }; @@ -2552,26 +2719,37 @@ export const cellSymbolHighlight = registerColor('notebook.symbolHighlightBackgr hc: null }, nls.localize('notebook.symbolHighlightBackground', "Background color of highlighted cell")); +export const cellEditorBackground = registerColor('notebook.cellEditorBackground', { + light: null, + dark: null, + hc: null +}, nls.localize('notebook.cellEditorBackground', "Cell editor background color.")); + registerThemingParticipant((theme, collector) => { - collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element, - .notebookOverlay > .cell-list-container > .notebook-gutter > .monaco-list > .monaco-scrollable-element { - padding-top: ${SCROLLABLE_ELEMENT_PADDING_TOP}px; - box-sizing: border-box; - }`); + // add css variable rules + + const focusedCellBorderColor = theme.getColor(focusedCellBorder); + const inactiveFocusedBorderColor = theme.getColor(inactiveFocusedCellBorder); + const selectedCellBorderColor = theme.getColor(selectedCellBorder); + collector.addRule(` + :root { + --notebook-focused-cell-border-color: ${focusedCellBorderColor}; + --notebook-inactive-focused-cell-border-color: ${inactiveFocusedBorderColor}; + --notebook-selected-cell-border-color: ${selectedCellBorderColor}; + } + `); + const link = theme.getColor(textLinkForeground); if (link) { - collector.addRule(`.notebookOverlay .output a, - .notebookOverlay .cell.markdown a, + collector.addRule(`.notebookOverlay .cell.markdown a, .notebookOverlay .output-show-more-container a { color: ${link};} `); } const activeLink = theme.getColor(textLinkActiveForeground); if (activeLink) { - collector.addRule(`.notebookOverlay .output a:hover, - .notebookOverlay .cell .output a:active, - .notebookOverlay .output-show-more-container a:active + collector.addRule(`.notebookOverlay .output-show-more-container a:active { color: ${activeLink}; }`); } const shortcut = theme.getColor(textPreformatForeground); @@ -2599,17 +2777,20 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.notebookOverlay .output-show-more-container { background-color: ${containerBackground}; }`); } - const editorBackgroundColor = theme.getColor(editorBackground); + const notebookBackground = theme.getColor(editorBackground); + if (notebookBackground) { + collector.addRule(`.notebookOverlay .cell-drag-image .cell-editor-container > div { background: ${notebookBackground} !important; }`); + collector.addRule(`.notebookOverlay .monaco-list-row .cell-title-toolbar { background-color: ${notebookBackground}; }`); + collector.addRule(`.notebookOverlay .monaco-list-row.cell-drag-image { background-color: ${notebookBackground}; }`); + collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .action-item { background-color: ${notebookBackground} }`); + collector.addRule(`.notebookOverlay .cell-list-top-cell-toolbar-container .action-item { background-color: ${notebookBackground} }`); + } + + const editorBackgroundColor = theme.getColor(cellEditorBackground) ?? theme.getColor(editorBackground); if (editorBackgroundColor) { collector.addRule(`.notebookOverlay .cell .monaco-editor-background, - .notebookOverlay .cell .margin-view-overlays, - .notebookOverlay .cell .cell-statusbar-container { background: ${editorBackgroundColor}; }`); - collector.addRule(`.notebookOverlay .cell-drag-image .cell-editor-container > div { background: ${editorBackgroundColor} !important; }`); - - collector.addRule(`.notebookOverlay .monaco-list-row .cell-title-toolbar { background-color: ${editorBackgroundColor}; }`); - collector.addRule(`.notebookOverlay .monaco-list-row.cell-drag-image { background-color: ${editorBackgroundColor}; }`); - collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .action-item { background-color: ${editorBackgroundColor} }`); - collector.addRule(`.notebookOverlay .cell-list-top-cell-toolbar-container .action-item { background-color: ${editorBackgroundColor} }`); + .notebookOverlay .cell .margin-view-overlays, + .notebookOverlay .cell .cell-statusbar-container { background: ${editorBackgroundColor}; }`); } const cellToolbarSeperator = theme.getColor(CELL_TOOLBAR_SEPERATOR); @@ -2623,8 +2804,8 @@ registerThemingParticipant((theme, collector) => { const focusedCellBackgroundColor = theme.getColor(focusedCellBackground); if (focusedCellBackgroundColor) { - collector.addRule(`.notebookOverlay .code-cell-row.focused .cell-focus-indicator, - .notebookOverlay .markdown-cell-row.focused { background-color: ${focusedCellBackgroundColor} !important; }`); + collector.addRule(`.notebookOverlay .code-cell-row.focused .cell-focus-indicator { background-color: ${focusedCellBackgroundColor} !important; }`); + collector.addRule(`.notebookOverlay .markdown-cell-row.focused { background-color: ${focusedCellBackgroundColor} !important; }`); collector.addRule(`.notebookOverlay .code-cell-row.focused .cell-collapsed-part { background-color: ${focusedCellBackgroundColor} !important; }`); } @@ -2656,30 +2837,6 @@ registerThemingParticipant((theme, collector) => { .notebookOverlay .code-cell-row:not(.focused).cell-output-hover .cell-collapsed-part { background-color: ${cellHoverBackgroundColor}; }`); } - const focusedCellBorderColor = theme.getColor(focusedCellBorder); - collector.addRule(` - .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-focus-indicator-top:before, - .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-focus-indicator-bottom:before, - .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-left:before, - .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-right:before { - 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 { - 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, - .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-editor-focus .cell-focus-indicator-bottom:before, - .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container.cell-editor-focus:before { - border-color: ${selectedCellBorderColor} !important; - }`); - const cellSymbolHighlightColor = theme.getColor(cellSymbolHighlight); if (cellSymbolHighlightColor) { collector.addRule(`.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.nb-symbolHighlight .cell-focus-indicator, @@ -2768,43 +2925,5 @@ registerThemingParticipant((theme, collector) => { }`); } - // Cell Margin - collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row div.cell.code { margin-left: ${CODE_CELL_LEFT_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .code-cell-row div.cell.code { margin-left: ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; }`); - collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell { margin-right: ${CELL_RIGHT_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .cell-inner-container { padding-top: ${CELL_TOP_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row > .cell-inner-container { padding-bottom: ${MARKDOWN_CELL_BOTTOM_MARGIN}px; padding-top: ${MARKDOWN_CELL_TOP_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row > .cell-inner-container.webview-backed-markdown-cell { padding: 0; }`); - collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row > .webview-backed-markdown-cell.markdown-cell-edit-mode .cell.code { padding-bottom: ${MARKDOWN_CELL_BOTTOM_MARGIN}px; padding-top: ${MARKDOWN_CELL_TOP_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .output { margin: 0px ${CELL_RIGHT_MARGIN}px 0px ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; }`); - collector.addRule(`.notebookOverlay .output { width: calc(100% - ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER + CELL_RIGHT_MARGIN}px); }`); - collector.addRule(`.notebookOverlay .output-show-more-container { margin: 0px ${CELL_RIGHT_MARGIN}px 0px ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; }`); - collector.addRule(`.notebookOverlay .output-show-more-container { width: calc(100% - ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER + CELL_RIGHT_MARGIN}px); }`); - - collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell.markdown { padding-left: ${CELL_RUN_GUTTER}px; }`); - collector.addRule(`.notebookOverlay .cell .run-button-container { width: 20px; left: ${CODE_CELL_LEFT_MARGIN + Math.floor(CELL_RUN_GUTTER - 20) / 2}px }`); - collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row :not(.webview-backed-markdown-cell) .cell-focus-indicator-top { height: ${CELL_TOP_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-side { bottom: ${BOTTOM_CELL_TOOLBAR_GAP}px; }`); - collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row.code-cell-row .cell-focus-indicator-left, - .notebookOverlay .monaco-list .monaco-list-row.code-cell-row .cell-drag-handle { width: ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; }`); - collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .cell-focus-indicator-left { width: ${CODE_CELL_LEFT_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator.cell-focus-indicator-right { width: ${CELL_RIGHT_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom { height: ${CELL_BOTTOM_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-shadow-container-bottom { top: ${CELL_BOTTOM_MARGIN}px; }`); - - collector.addRule(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-collapsed-part { margin-left: ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; height: ${COLLAPSED_INDICATOR_HEIGHT}px; }`); - collector.addRule(`.notebookOverlay .cell-list-top-cell-toolbar-container { top: -${SCROLLABLE_ELEMENT_PADDING_TOP}px }`); - - collector.addRule(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { height: ${BOTTOM_CELL_TOOLBAR_HEIGHT}px }`); - collector.addRule(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .cell-list-top-cell-toolbar-container { height: ${BOTTOM_CELL_TOOLBAR_HEIGHT}px }`); - - // left and right border margins - collector.addRule(` - .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-left:before, - .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-right:before, - .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-left:before, - .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-right:before { - top: -${CELL_TOP_MARGIN}px; height: calc(100% + ${CELL_TOP_MARGIN + CELL_BOTTOM_MARGIN}px) - }`); }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetContextKeys.ts index ba83b21978..3b7b5d917f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetContextKeys.ts @@ -1,11 +1,11 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * 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 { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ICellViewModel, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_KERNEL_COUNT, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICellViewModel, INotebookEditor, NOTEBOOK_HAS_OUTPUTS, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_KERNEL_SELECTED, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON, NOTEBOOK_VIEW_TYPE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; @@ -13,12 +13,17 @@ import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/not export class NotebookEditorContextKeys { private readonly _notebookKernelCount: IContextKey; + private readonly _notebookKernelSelected: IContextKey; private readonly _interruptibleKernel: IContextKey; private readonly _someCellRunning: IContextKey; + private readonly _hasOutputs: IContextKey; + private readonly _useConsolidatedOutputButton: IContextKey; + private _viewType!: IContextKey; private readonly _disposables = new DisposableStore(); private readonly _viewModelDisposables = new DisposableStore(); private readonly _cellStateListeners: IDisposable[] = []; + private readonly _cellOutputsListeners: IDisposable[] = []; constructor( private readonly _editor: INotebookEditor, @@ -26,13 +31,22 @@ export class NotebookEditorContextKeys { @IContextKeyService contextKeyService: IContextKeyService, ) { this._notebookKernelCount = NOTEBOOK_KERNEL_COUNT.bindTo(contextKeyService); + this._notebookKernelSelected = NOTEBOOK_KERNEL_SELECTED.bindTo(contextKeyService); this._interruptibleKernel = NOTEBOOK_INTERRUPTIBLE_KERNEL.bindTo(contextKeyService); this._someCellRunning = NOTEBOOK_HAS_RUNNING_CELL.bindTo(contextKeyService); + this._useConsolidatedOutputButton = NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON.bindTo(contextKeyService); + this._hasOutputs = NOTEBOOK_HAS_OUTPUTS.bindTo(contextKeyService); + this._viewType = NOTEBOOK_VIEW_TYPE.bindTo(contextKeyService); + + this._handleDidChangeModel(); + this._updateForNotebookOptions(); this._disposables.add(_editor.onDidChangeModel(this._handleDidChangeModel, this)); this._disposables.add(_notebookKernelService.onDidAddKernel(this._updateKernelContext, this)); this._disposables.add(_notebookKernelService.onDidChangeNotebookKernelBinding(this._updateKernelContext, this)); - this._handleDidChangeModel(); + this._disposables.add(_editor.notebookOptions.onDidChangeOptions(() => { + this._updateForNotebookOptions(); + })); } dispose(): void { @@ -41,6 +55,11 @@ export class NotebookEditorContextKeys { this._notebookKernelCount.reset(); this._interruptibleKernel.reset(); this._someCellRunning.reset(); + this._viewType.reset(); + dispose(this._cellStateListeners); + this._cellStateListeners.length = 0; + dispose(this._cellOutputsListeners); + this._cellOutputsListeners.length = 0; } private _handleDidChangeModel(): void { @@ -50,6 +69,8 @@ export class NotebookEditorContextKeys { this._viewModelDisposables.clear(); dispose(this._cellStateListeners); this._cellStateListeners.length = 0; + dispose(this._cellOutputsListeners); + this._cellOutputsListeners.length = 0; if (!this._editor.hasModel()) { return; @@ -62,26 +83,52 @@ export class NotebookEditorContextKeys { if (!e.runStateChanged) { return; } - if (c.metadata?.runState === NotebookCellExecutionState.Pending) { + if (c.internalMetadata.runState === NotebookCellExecutionState.Pending) { executionCount++; - } else if (c.metadata?.runState === NotebookCellExecutionState.Idle) { + } else if (!c.internalMetadata.runState) { executionCount--; } this._someCellRunning.set(executionCount > 0); }); }; + const recomputeOutputsExistence = () => { + let hasOutputs = false; + if (this._editor.hasModel()) { + for (let i = 0; i < this._editor.viewModel.viewCells.length; i++) { + if (this._editor.viewModel.viewCells[i].outputsViewModels.length > 0) { + hasOutputs = true; + break; + } + } + } + + this._hasOutputs.set(hasOutputs); + }; + + const addCellOutputsListener = (c: ICellViewModel) => { + return c.model.onDidChangeOutputs(() => { + recomputeOutputsExistence(); + }); + }; + for (const cell of this._editor.viewModel.viewCells) { this._cellStateListeners.push(addCellStateListener(cell)); + this._cellOutputsListeners.push(addCellOutputsListener(cell)); } + recomputeOutputsExistence(); + this._viewModelDisposables.add(this._editor.viewModel.onDidChangeViewCells(e => { e.splices.reverse().forEach(splice => { const [start, deleted, newCells] = splice; - const deletedCells = this._cellStateListeners.splice(start, deleted, ...newCells.map(addCellStateListener)); - dispose(deletedCells); + const deletedCellStates = this._cellStateListeners.splice(start, deleted, ...newCells.map(addCellStateListener)); + const deletedCellOutputStates = this._cellOutputsListeners.splice(start, deleted, ...newCells.map(addCellOutputsListener)); + dispose(deletedCellStates); + dispose(deletedCellOutputStates); }); })); + this._viewType.set(this._editor.viewModel.viewType); } private _updateKernelContext(): void { @@ -94,5 +141,10 @@ export class NotebookEditorContextKeys { const { selected, all } = this._notebookKernelService.getMatchingKernel(this._editor.viewModel.notebookDocument); this._notebookKernelCount.set(all.length); this._interruptibleKernel.set(selected?.implementsInterrupt ?? false); + this._notebookKernelSelected.set(Boolean(selected)); + } + + private _updateForNotebookOptions(): void { + this._useConsolidatedOutputButton.set(this._editor.notebookOptions.getLayoutConfiguration().consolidatedOutputButton); } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts index 4b84437d2f..17a20162a6 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts @@ -11,6 +11,8 @@ export const configureKernelIcon = registerIcon('notebook-kernel-configure', Cod 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.')); +export const executeAboveIcon = registerIcon('notebook-execute-above', Codicon.runAbove, localize('executeAboveIcon', 'Icon to execute above cells in notebook editors.')); +export const executeBelowIcon = registerIcon('notebook-execute-below', Codicon.runBelow, localize('executeBelowIcon', 'Icon to execute below cells in notebook editors.')); export const stopIcon = registerIcon('notebook-stop', Codicon.primitiveSquare, localize('stopIcon', 'Icon to stop an execution in notebook editors.')); export const deleteCellIcon = registerIcon('notebook-delete-cell', Codicon.trash, localize('deleteCellIcon', 'Icon to delete a cell in notebook editors.')); export const executeAllIcon = registerIcon('notebook-execute-all', Codicon.runAll, localize('executeAllIcon', 'Icon to execute all cells in notebook editors.')); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookKernelActionViewItem.ts b/src/vs/workbench/contrib/notebook/browser/notebookKernelActionViewItem.ts new file mode 100644 index 0000000000..721cebbe7b --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookKernelActionViewItem.ts @@ -0,0 +1,102 @@ +/*--------------------------------------------------------------------------------------------- + * 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/notebookKernelActionViewItem'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { Action, IAction } from 'vs/base/common/actions'; +import { localize } from 'vs/nls'; +import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; +import { selectKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { INotebookKernelMatchResult, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { toolbarHoverBackground } from 'vs/platform/theme/common/colorRegistry'; +import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; + +registerThemingParticipant((theme, collector) => { + const value = theme.getColor(toolbarHoverBackground); + collector.addRule(`:root { + --code-toolbarHoverBackground: ${value}; + }`); +}); + +export class NotebooKernelActionViewItem extends ActionViewItem { + + private _kernelLabel?: HTMLAnchorElement; + + constructor( + actualAction: IAction, + private readonly _editor: NotebookEditor | INotebookEditor, + @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, + ) { + super( + undefined, + new Action('fakeAction', undefined, ThemeIcon.asClassName(selectKernelIcon), true, (event) => actualAction.run(event)), + { label: false, icon: true } + ); + this._register(_editor.onDidChangeModel(this._update, this)); + this._register(_notebookKernelService.onDidChangeNotebookAffinity(this._update, this)); + this._register(_notebookKernelService.onDidChangeNotebookKernelBinding(this._update, this)); + } + + override render(container: HTMLElement): void { + this._update(); + super.render(container); + container.classList.add('kernel-action-view-item'); + this._kernelLabel = document.createElement('a'); + container.appendChild(this._kernelLabel); + this.updateLabel(); + } + + override updateLabel() { + if (this._kernelLabel) { + this._kernelLabel.classList.add('kernel-label'); + this._kernelLabel.innerText = this._action.label; + this._kernelLabel.title = this._action.tooltip; + } + } + + protected _update(): void { + const notebook = this._editor.viewModel?.notebookDocument; + + if (!notebook) { + this._resetAction(); + return; + } + + const info = this._notebookKernelService.getMatchingKernel(notebook); + this._updateActionFromKernelInfo(info); + } + + private _updateActionFromKernelInfo(info: INotebookKernelMatchResult): void { + + if (info.all.length === 0) { + // should not happen - means "bad" context keys + this._resetAction(); + return; + } + + this._action.enabled = true; + const selectedOrSuggested = info.selected ?? info.suggested; + if (selectedOrSuggested) { + // selected or suggested kernel + this._action.label = selectedOrSuggested.label; + this._action.tooltip = selectedOrSuggested.description ?? selectedOrSuggested.detail ?? ''; + if (!info.selected) { + // special UI for selected kernel? + } + + } else { + // many kernels + this._action.label = localize('select', "Select Kernel"); + this._action.tooltip = ''; + } + } + + private _resetAction(): void { + this._action.enabled = false; + this._action.label = ''; + this._action.class = ''; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts index 52cdfeadee..ef1e606a27 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Event, Emitter } from 'vs/base/common/event'; @@ -74,7 +74,7 @@ export class NotebookKernelService implements INotebookKernelService { // auto associate kernels to new notebook documents, also emit event when // a notebook has been closed (but don't update the memento) this._disposables.add(_notebookService.onDidAddNotebookDocument(this._tryAutoBindNotebook, this)); - this._disposables.add(_notebookService.onDidRemoveNotebookDocument(notebook => { + this._disposables.add(_notebookService.onWillRemoveNotebookDocument(notebook => { const kernelId = this._notebookBindings.get(NotebookTextModelLikeId.str(notebook)); if (kernelId) { this._onDidChangeNotebookKernelBinding.fire({ notebook: notebook.uri, oldKernel: kernelId, newKernel: undefined }); @@ -191,7 +191,7 @@ export class NotebookKernelService implements INotebookKernelService { const selectedId = this._notebookBindings.get(NotebookTextModelLikeId.str(notebook)); const selected = selectedId ? this._kernels.get(selectedId)?.kernel : undefined; - return { all, selected }; + return { all, selected, suggested: all.length === 1 ? all[0] : undefined }; } // default kernel for notebookType diff --git a/src/vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl.ts new file mode 100644 index 0000000000..fbd7c606dd --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { INotebookRendererMessagingService, IScopedRendererMessaging } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +type MessageToSend = { editorId: string; rendererId: string; message: unknown }; + +export class NotebookRendererMessagingService implements INotebookRendererMessagingService { + declare _serviceBrand: undefined; + /** + * Activation promises. Maps renderer IDs to a queue of messages that should + * be sent once activation finishes, or undefined if activation is complete. + */ + private readonly activations = new Map(); + private readonly receiveMessageEmitter = new Emitter<{ editorId: string; rendererId: string, message: unknown }>(); + public readonly onDidReceiveMessage = this.receiveMessageEmitter.event; + private readonly postMessageEmitter = new Emitter(); + public readonly onShouldPostMessage = this.postMessageEmitter.event; + + constructor(@IExtensionService private readonly extensionService: IExtensionService) { } + + /** @inheritdoc */ + public fireDidReceiveMessage(editorId: string, rendererId: string, message: unknown): void { + this.receiveMessageEmitter.fire({ editorId, rendererId, message }); + } + + /** @inheritdoc */ + public prepare(rendererId: string) { + if (this.activations.has(rendererId)) { + return; + } + + const queue: MessageToSend[] = []; + this.activations.set(rendererId, queue); + + this.extensionService.activateByEvent(`onRenderer:${rendererId}`).then(() => { + for (const message of queue) { + this.postMessageEmitter.fire(message); + } + + this.activations.set(rendererId, undefined); + }); + } + + /** @inheritdoc */ + public getScoped(editorId: string): IScopedRendererMessaging { + return { + onDidReceiveMessage: Event.filter(this.onDidReceiveMessage, e => e.editorId === editorId), + postMessage: (rendererId, message) => this.postMessage(editorId, rendererId, message), + }; + } + + private postMessage(editorId: string, rendererId: string, message: unknown): void { + if (!this.activations.has(rendererId)) { + this.prepare(rendererId); + } + + const activation = this.activations.get(rendererId); + const toSend = { rendererId, editorId, message }; + if (activation === undefined) { + this.postMessageEmitter.fire(toSend); + } else { + activation.push(toSend); + } + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 9b42f20dd1..a3bc13b48f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -3,43 +3,44 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as glob from 'vs/base/common/glob'; -import { localize } from 'vs/nls'; import { getPixelRatio, getZoomLevel } from 'vs/base/browser/browser'; import { Emitter, Event } from 'vs/base/common/event'; +import * as glob from 'vs/base/common/glob'; import { Iterable } from 'vs/base/common/iterator'; +import { Lazy } from 'vs/base/common/lazy'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; +import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol'; +import { IEditorInput } from 'vs/workbench/common/editor'; +import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { Memento } from 'vs/workbench/common/memento'; -import { INotebookEditorContribution, notebookMarkupRendererExtensionPoint, notebookProviderExtensionPoint, notebookRendererExtensionPoint } from 'vs/workbench/contrib/notebook/browser/extensionPoint'; -import { NotebookEditorOptions, updateEditorTopPadding } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditorContribution, notebooksExtensionPoint, notebookRendererExtensionPoint } from 'vs/workbench/contrib/notebook/browser/extensionPoint'; +import { INotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookDiffEditorInput'; 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, CellUri, DisplayOrderKey, INotebookExclusiveDocumentFilter, INotebookMarkupRendererInfo, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, IOutputDto, mimeTypeIsAlwaysSecure, mimeTypeSupportedByCore, NotebookDataDto, NotebookEditorPriority, NotebookRendererMatch, NotebookTextDiffEditorPreview, RENDERER_NOT_AVAILABLE, sortMimeTypes, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NotebookMarkupRendererInfo as NotebookMarkupRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookMarkdownRenderer'; +import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, BUILTIN_RENDERER_ID, CellUri, DisplayOrderKey, INotebookExclusiveDocumentFilter, INotebookContributionData, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, IOutputDto, mimeTypeIsAlwaysSecure, mimeTypeSupportedByCore, NotebookDataDto, NotebookEditorPriority, NotebookRendererMatch, NotebookTextDiffEditorPreview, RENDERER_NOT_AVAILABLE, sortMimeTypes, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; +import { updateEditorTopPadding } from 'vs/workbench/contrib/notebook/common/notebookOptions'; import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer'; import { NotebookEditorDescriptor, NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { ComplexNotebookProviderInfo, INotebookContentProvider, INotebookSerializer, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { ContributedEditorPriority, DiffEditorInputFactoryFunction, EditorInputFactoryFunction, IEditorOverrideService, IEditorType } from 'vs/workbench/services/editor/common/editorOverrideService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { Schemas } from 'vs/base/common/network'; -import { Lazy } from 'vs/base/common/lazy'; -import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; -import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookDiffEditorInput'; -import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; -import { ContributedEditorPriority, IEditorAssociationsRegistry, IEditorOverrideService, IEditorType, IEditorTypesHandler } from 'vs/workbench/services/editor/common/editorOverrideService'; -import { EditorExtensions } from 'vs/workbench/common/editor'; -import { IFileService } from 'vs/platform/files/common/files'; export class NotebookProviderInfoStore extends Disposable { @@ -79,7 +80,7 @@ export class NotebookProviderInfoStore extends Disposable { } })); - notebookProviderExtensionPoint.setHandler(extensions => this._setupHandler(extensions)); + notebooksExtensionPoint.setHandler(extensions => this._setupHandler(extensions)); } override dispose(): void { @@ -93,16 +94,24 @@ export class NotebookProviderInfoStore extends Disposable { for (const extension of extensions) { for (const notebookContribution of extension.value) { + + if (!notebookContribution.type) { + extension.collector.error(`Notebook does not specify type-property`); + continue; + } + + if (this.get(notebookContribution.type)) { + extension.collector.error(`Notebook type '${notebookContribution.type}' already used`); + continue; + } + this.add(new NotebookProviderInfo({ - id: notebookContribution.viewType, + extension: extension.description.identifier, + id: notebookContribution.type, displayName: notebookContribution.displayName, selectors: notebookContribution.selector || [], priority: this._convertPriority(notebookContribution.priority), - providerExtensionId: extension.description.identifier.value, - providerDescription: extension.description.description, providerDisplayName: extension.description.isBuiltin ? localize('builtinProviderDisplayName', "Built-in") : extension.description.displayName || extension.description.identifier.value, - providerExtensionLocation: extension.description.extensionLocation, - dynamicContribution: false, exclusive: false })); } @@ -126,46 +135,63 @@ export class NotebookProviderInfoStore extends Disposable { } - private _registerContributionPoint(notebookProviderInfo: NotebookProviderInfo): void { + private _registerContributionPoint(notebookProviderInfo: NotebookProviderInfo): IDisposable { + + const disposables = new DisposableStore(); + for (const selector of notebookProviderInfo.selectors) { const globPattern = (selector as INotebookExclusiveDocumentFilter).include || selector as glob.IRelativePattern | string; - this._contributedEditorDisposables.add(this._editorOverrideService.registerContributionPoint( - globPattern, - { - id: notebookProviderInfo.id, - label: notebookProviderInfo.displayName, - detail: notebookProviderInfo.providerDisplayName, - describes: (currentEditor) => currentEditor instanceof NotebookEditorInput && currentEditor.viewType === notebookProviderInfo.id, - priority: notebookProviderInfo.exclusive ? ContributedEditorPriority.exclusive : notebookProviderInfo.priority, - }, - { - canHandleDiff: () => !!this._configurationService.getValue(NotebookTextDiffEditorPreview) && !this._accessibilityService.isScreenReaderOptimized(), - canSupportResource: resource => resource.scheme === Schemas.untitled || resource.scheme === Schemas.vscodeNotebookCell || this._fileService.canHandleResource(resource) - }, - (resource, options, group) => { - const data = CellUri.parse(resource); - let notebookUri: URI = resource; - let cellOptions: IResourceEditorInput | undefined; + const notebookEditorInfo = { + id: notebookProviderInfo.id, + label: notebookProviderInfo.displayName, + detail: notebookProviderInfo.providerDisplayName, + describes: (currentEditor: IEditorInput) => currentEditor instanceof NotebookEditorInput && currentEditor.viewType === notebookProviderInfo.id, + priority: notebookProviderInfo.exclusive ? ContributedEditorPriority.exclusive : notebookProviderInfo.priority, + }; + const notebookEditorOptions = { + canHandleDiff: () => !!this._configurationService.getValue(NotebookTextDiffEditorPreview) && !this._accessibilityService.isScreenReaderOptimized(), + canSupportResource: (resource: URI) => resource.scheme === Schemas.untitled || resource.scheme === Schemas.vscodeNotebookCell || this._fileService.canHandleResource(resource) + }; + const notebookEditorInputFactory: EditorInputFactoryFunction = (resource, options, group) => { + const data = CellUri.parse(resource); + let notebookUri: URI = resource; + let cellOptions: IResourceEditorInput | undefined; - if (data) { - notebookUri = data.notebook; - cellOptions = { resource: resource }; - } - - const notebookOptions = new NotebookEditorOptions({ ...options, cellOptions }); - return { editor: NotebookEditorInput.create(this._instantiationService, notebookUri, notebookProviderInfo.id), options: notebookOptions }; - }, - (diffEditorInput, group) => { - const modifiedInput = diffEditorInput.modifiedInput; - const originalInput = diffEditorInput.originalInput; - const notebookUri = modifiedInput.resource!; - const originalNotebookUri = originalInput.resource!; - return { editor: NotebookDiffEditorInput.create(this._instantiationService, notebookUri, modifiedInput.getName(), originalNotebookUri, originalInput.getName(), diffEditorInput.getName(), notebookProviderInfo.id) }; + if (data) { + notebookUri = data.notebook; + cellOptions = { resource, options }; } + + const notebookOptions: INotebookEditorOptions = { ...options, cellOptions }; + return { editor: NotebookEditorInput.create(this._instantiationService, notebookUri, notebookProviderInfo.id), options: notebookOptions }; + }; + const notebookEditorDiffFactory: DiffEditorInputFactoryFunction = (diffEditorInput: DiffEditorInput, options, group) => { + const modifiedInput = diffEditorInput.modifiedInput; + const originalInput = diffEditorInput.originalInput; + const notebookUri = modifiedInput.resource!; + const originalNotebookUri = originalInput.resource!; + return { editor: NotebookDiffEditorInput.create(this._instantiationService, notebookUri, modifiedInput.getName(), originalNotebookUri, originalInput.getName(), diffEditorInput.getName(), notebookProviderInfo.id) }; + }; + // Register the notebook editor + disposables.add(this._editorOverrideService.registerEditor( + globPattern, + notebookEditorInfo, + notebookEditorOptions, + notebookEditorInputFactory, + notebookEditorDiffFactory + )); + // Then register the schema handler as exclusive for that notebook + disposables.add(this._editorOverrideService.registerEditor( + `${Schemas.vscodeNotebookCell}:/**/${globPattern}`, + { ...notebookEditorInfo, priority: ContributedEditorPriority.exclusive }, + notebookEditorOptions, + notebookEditorInputFactory, + notebookEditorDiffFactory )); } - } + return disposables; + } private _clear(): void { this._contributedEditors.clear(); @@ -176,16 +202,25 @@ export class NotebookProviderInfoStore extends Disposable { return this._contributedEditors.get(viewType); } - add(info: NotebookProviderInfo): void { + add(info: NotebookProviderInfo): IDisposable { if (this._contributedEditors.has(info.id)) { - return; + throw new Error(`notebook type '${info.id}' ALREADY EXISTS`); } this._contributedEditors.set(info.id, info); - this._registerContributionPoint(info); + const editorRegistration = this._registerContributionPoint(info); + this._contributedEditorDisposables.add(editorRegistration); const mementoObject = this._memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = Array.from(this._contributedEditors.values()); this._memento.saveMemento(); + + return toDisposable(() => { + const mementoObject = this._memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = Array.from(this._contributedEditors.values()); + this._memento.saveMemento(); + editorRegistration.dispose(); + this._contributedEditors.delete(info.id); + }); } getContributedNotebook(resource: URI): readonly NotebookProviderInfo[] { @@ -224,6 +259,10 @@ export class NotebookOutputRendererInfoStore { return this.contributedRenderers.get(rendererId); } + getAll(): NotebookOutputRendererInfo[] { + return Array.from(this.contributedRenderers.values()); + } + add(info: NotebookOutputRendererInfo): void { if (this.contributedRenderers.has(info.id)) { return; @@ -269,23 +308,27 @@ class ModelData implements IDisposable { } } -export class NotebookService extends Disposable implements INotebookService, IEditorTypesHandler { +export class NotebookService extends Disposable implements INotebookService { declare readonly _serviceBrand: undefined; private readonly _notebookProviders = new Map(); private readonly _notebookProviderInfoStore: NotebookProviderInfoStore; private readonly _notebookRenderersInfoStore = this._instantiationService.createInstance(NotebookOutputRendererInfoStore); - private readonly _markdownRenderersInfos = new Set(); private readonly _models = new ResourceMap(); - private readonly _onDidCreateNotebookDocument = this._register(new Emitter()); + private readonly _onWillAddNotebookDocument = this._register(new Emitter()); private readonly _onDidAddNotebookDocument = this._register(new Emitter()); + private readonly _onWillRemoveNotebookDocument = this._register(new Emitter()); private readonly _onDidRemoveNotebookDocument = this._register(new Emitter()); - readonly onDidCreateNotebookDocument = this._onDidCreateNotebookDocument.event; + readonly onWillAddNotebookDocument = this._onWillAddNotebookDocument.event; readonly onDidAddNotebookDocument = this._onDidAddNotebookDocument.event; readonly onDidRemoveNotebookDocument = this._onDidRemoveNotebookDocument.event; + readonly onWillRemoveNotebookDocument = this._onWillRemoveNotebookDocument.event; + + private readonly _onWillRemoveViewType = this._register(new Emitter()); + readonly onWillRemoveViewType = this._onWillRemoveViewType.event; private readonly _onDidChangeEditorTypes = this._register(new Emitter()); onDidChangeEditorTypes: Event = this._onDidChangeEditorTypes.event; @@ -302,6 +345,7 @@ export class NotebookService extends Disposable implements INotebookService, IEd @IInstantiationService private readonly _instantiationService: IInstantiationService, @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, ) { super(); @@ -315,13 +359,13 @@ export class NotebookService extends Disposable implements INotebookService, IEd for (const extension of renderers) { for (const notebookContribution of extension.value) { if (!notebookContribution.entrypoint) { // avoid crashing - console.error(`Cannot register renderer for ${extension.description.identifier.value} since it did not have an entrypoint. This is now required: https://github.com/microsoft/vscode/issues/102644`); + extension.collector.error(`Notebook renderer does not specify entry point`); continue; } - const id = notebookContribution.id ?? notebookContribution.viewType; + const id = notebookContribution.id; if (!id) { - console.error(`Notebook renderer from ${extension.description.identifier.value} is missing an 'id'`); + extension.collector.error(`Notebook renderer does not specify id-property`); continue; } @@ -333,44 +377,11 @@ export class NotebookService extends Disposable implements INotebookService, IEd mimeTypes: notebookContribution.mimeTypes || [], dependencies: notebookContribution.dependencies, optionalDependencies: notebookContribution.optionalDependencies, + requiresMessaging: notebookContribution.requiresMessaging, })); } } }); - notebookMarkupRendererExtensionPoint.setHandler((renderers) => { - this._markdownRenderersInfos.clear(); - - for (const extension of renderers) { - if (!extension.description.enableProposedApi && !extension.description.isBuiltin) { - // Only allow proposed extensions to use this extension point - return; - } - - for (const notebookContribution of extension.value) { - if (!notebookContribution.entrypoint) { // avoid crashing - console.error(`Cannot register renderer for ${extension.description.identifier.value} since it did not have an entrypoint. This is now required: https://github.com/microsoft/vscode/issues/102644`); - continue; - } - - const id = notebookContribution.id; - if (!id) { - console.error(`Notebook renderer from ${extension.description.identifier.value} is missing an 'id'`); - continue; - } - - this._markdownRenderersInfos.add(new NotebookMarkupRendererInfo({ - id, - extension: extension.description, - entrypoint: notebookContribution.entrypoint, - displayName: notebookContribution.displayName, - mimeTypes: notebookContribution.mimeTypes, - dependsOn: notebookContribution.dependsOn, - })); - } - } - }); - - this._register(Registry.as(EditorExtensions.Associations).registerEditorTypesHandler('Notebook', this)); const updateOrder = () => { const userOrder = this._configurationService.getValue(DisplayOrderKey); @@ -455,51 +466,50 @@ export class NotebookService extends Disposable implements INotebookService, IEd return this._notebookProviders.has(viewType); } - private _registerProviderData(viewType: string, data: SimpleNotebookProviderInfo | ComplexNotebookProviderInfo): void { - if (this._notebookProviders.has(viewType)) { - throw new Error(`notebook controller for viewtype '${viewType}' already exists`); - } - this._notebookProviders.set(viewType, data); - } + registerContributedNotebookType(viewType: string, data: INotebookContributionData): IDisposable { - registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: INotebookContentProvider): IDisposable { - this._registerProviderData(viewType, new ComplexNotebookProviderInfo(viewType, controller, extensionData)); - if (controller.viewOptions && !this._notebookProviderInfoStore.get(viewType)) { - // register this content provider to the static contribution, if it does not exist - const info = new NotebookProviderInfo({ - displayName: controller.viewOptions.displayName, - id: viewType, - priority: ContributedEditorPriority.default, - selectors: [], - providerExtensionId: extensionData.id.value, - providerDescription: extensionData.description, - providerDisplayName: extensionData.id.value, - providerExtensionLocation: URI.revive(extensionData.location), - dynamicContribution: true, - exclusive: controller.viewOptions.exclusive - }); + const info = new NotebookProviderInfo({ + extension: data.extension, + id: viewType, + displayName: data.displayName, + providerDisplayName: data.providerDisplayName, + exclusive: data.exclusive, + priority: ContributedEditorPriority.default, + selectors: [], + }); - info.update({ selectors: controller.viewOptions.filenamePattern }); - info.update({ options: controller.options }); - this._notebookProviderInfoStore.add(info); - } - - this._notebookProviderInfoStore.get(viewType)?.update({ options: controller.options }); + info.update({ selectors: data.filenamePattern }); + const reg = this._notebookProviderInfoStore.add(info); this._onDidChangeEditorTypes.fire(); + return toDisposable(() => { - this._notebookProviders.delete(viewType); + reg.dispose(); this._onDidChangeEditorTypes.fire(); }); } - registerNotebookSerializer(viewType: string, extensionData: NotebookExtensionDescription, serializer: INotebookSerializer): IDisposable { - this._registerProviderData(viewType, new SimpleNotebookProviderInfo(viewType, serializer, extensionData)); + private _registerProviderData(viewType: string, data: SimpleNotebookProviderInfo | ComplexNotebookProviderInfo): IDisposable { + if (this._notebookProviders.has(viewType)) { + throw new Error(`notebook controller for viewtype '${viewType}' already exists`); + } + this._notebookProviders.set(viewType, data); return toDisposable(() => { + this._onWillRemoveViewType.fire(viewType); this._notebookProviders.delete(viewType); }); } + registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: INotebookContentProvider): IDisposable { + this._notebookProviderInfoStore.get(viewType)?.update({ options: controller.options }); + return this._registerProviderData(viewType, new ComplexNotebookProviderInfo(viewType, controller, extensionData)); + } + + registerNotebookSerializer(viewType: string, extensionData: NotebookExtensionDescription, serializer: INotebookSerializer): IDisposable { + this._notebookProviderInfoStore.get(viewType)?.update({ options: serializer.options }); + return this._registerProviderData(viewType, new SimpleNotebookProviderInfo(viewType, serializer, extensionData)); + } + async withNotebookDataProvider(resource: URI, viewType?: string): Promise { const providers = this._notebookProviderInfoStore.getContributedNotebook(resource); // If we have a viewtype specified we want that data provider, as the resource won't always map correctly @@ -523,8 +533,8 @@ export class NotebookService extends Disposable implements INotebookService, IEd this._notebookRenderersInfoStore.setPreferred(mimeType, rendererId); } - getMarkupRendererInfo(): INotebookMarkupRendererInfo[] { - return Array.from(this._markdownRenderersInfos); + getRenderers(): INotebookRendererInfo[] { + return this._notebookRenderersInfoStore.getAll(); } // --- notebook documents: create, destory, retrieve, enumerate @@ -535,7 +545,7 @@ export class NotebookService extends Disposable implements INotebookService, IEd } const notebookModel = this._instantiationService.createInstance(NotebookTextModel, viewType, uri, data.cells, data.metadata, transientOptions); this._models.set(uri, new ModelData(notebookModel, this._onWillDisposeDocument.bind(this))); - this._onDidCreateNotebookDocument.fire(notebookModel); + this._onWillAddNotebookDocument.fire(notebookModel); this._onDidAddNotebookDocument.fire(notebookModel); return notebookModel; } @@ -555,6 +565,7 @@ export class NotebookService extends Disposable implements INotebookService, IEd private _onWillDisposeDocument(model: INotebookTextModel): void { const modelData = this._models.get(model.uri); if (modelData) { + this._onWillRemoveNotebookDocument.fire(modelData.model); this._models.delete(model.uri); modelData.dispose(); this._onDidRemoveNotebookDocument.fire(modelData.model); @@ -585,14 +596,14 @@ export class NotebookService extends Disposable implements INotebookService, IEd orderMimeTypes.push({ mimeType: mimeType, rendererId: handler.id, - isTrusted: textModel.metadata.trusted + isTrusted: true }); for (let i = 1; i < handlers.length; i++) { orderMimeTypes.push({ mimeType: mimeType, rendererId: handlers[i].id, - isTrusted: textModel.metadata.trusted + isTrusted: true }); } @@ -600,7 +611,7 @@ export class NotebookService extends Disposable implements INotebookService, IEd orderMimeTypes.push({ mimeType: mimeType, rendererId: BUILTIN_RENDERER_ID, - isTrusted: mimeTypeIsAlwaysSecure(mimeType) || textModel.metadata.trusted + isTrusted: mimeTypeIsAlwaysSecure(mimeType) || this.workspaceTrustManagementService.isWorkpaceTrusted() }); } } else { @@ -608,13 +619,13 @@ export class NotebookService extends Disposable implements INotebookService, IEd orderMimeTypes.push({ mimeType: mimeType, rendererId: BUILTIN_RENDERER_ID, - isTrusted: mimeTypeIsAlwaysSecure(mimeType) || textModel.metadata.trusted + isTrusted: mimeTypeIsAlwaysSecure(mimeType) || this.workspaceTrustManagementService.isWorkpaceTrusted() }); } else { orderMimeTypes.push({ mimeType: mimeType, rendererId: RENDERER_NOT_AVAILABLE, - isTrusted: textModel.metadata.trusted + isTrusted: true }); } } @@ -627,7 +638,7 @@ export class NotebookService extends Disposable implements INotebookService, IEd return this._notebookRenderersInfoStore.getContributedRenderer(mimeType, kernelProvides); } - getContributedNotebookProviders(resource?: URI): readonly NotebookProviderInfo[] { + getContributedNotebookTypes(resource?: URI): readonly NotebookProviderInfo[] { if (resource) { return this._notebookProviderInfoStore.getContributedNotebook(resource); } @@ -635,7 +646,7 @@ export class NotebookService extends Disposable implements INotebookService, IEd return [...this._notebookProviderInfoStore]; } - getContributedNotebookProvider(viewType: string): NotebookProviderInfo | undefined { + getContributedNotebookType(viewType: string): NotebookProviderInfo | undefined { return this._notebookProviderInfoStore.get(viewType); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index a11ea4f92c..6a3bf97224 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -24,8 +24,8 @@ import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/ import { diff, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange, cellRangesToIndexes } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { clamp } from 'vs/base/common/numbers'; -import { SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; import { ISplice } from 'vs/base/common/sequence'; +import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; export interface IFocusNextPreviousDelegate { onFocusNext(applyFocusNext: () => void): void; @@ -91,10 +91,13 @@ export class NotebookCellList extends WorkbenchList implements ID private readonly _focusNextPreviousDelegate: IFocusNextPreviousDelegate; + private readonly _viewContext: ViewContext; + constructor( private listUser: string, parentContainer: HTMLElement, container: HTMLElement, + viewContext: ViewContext, delegate: IListVirtualDelegate, renderers: IListRenderer[], contextKeyService: IContextKeyService, @@ -106,6 +109,7 @@ export class NotebookCellList extends WorkbenchList implements ID ) { super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, keybindingService); NOTEBOOK_CELL_LIST_FOCUSED.bindTo(this.contextKeyService).set(true); + this._viewContext = viewContext; this._focusNextPreviousDelegate = options.focusNextPreviousDelegate; this._previousFocusedElements = this.getFocusedElements(); this._localDisposableStore.add(this.onDidChangeFocus((e) => { @@ -177,7 +181,7 @@ export class NotebookCellList extends WorkbenchList implements ID this._localDisposableStore.add(this.view.onMouseDblClick(() => { const focus = this.getFocusedElements()[0]; - if (focus && focus.cellKind === CellKind.Markdown && !focus.metadata?.inputCollapsed) { + if (focus && focus.cellKind === CellKind.Markup && !focus.metadata.inputCollapsed) { focus.updateEditState(CellEditState.Editing, 'dbclick'); focus.focusMode = CellFocusMode.Editor; } @@ -900,7 +904,8 @@ export class NotebookCellList extends WorkbenchList implements ID } getViewScrollBottom() { - return this.getViewScrollTop() + this.view.renderHeight - SCROLLABLE_ELEMENT_PADDING_TOP; + const topInsertToolbarHeight = this._viewContext.notebookOptions.computeTopInserToolbarHeight(this.viewModel?.viewType); + return this.getViewScrollTop() + this.view.renderHeight - topInsertToolbarHeight; } private _revealRange(viewIndex: number, range: Range, revealType: CellRevealType, newlyCreated: boolean, alignToBottom: boolean) { @@ -1279,6 +1284,13 @@ export class NotebookCellList extends WorkbenchList implements ID this._viewModelStore.dispose(); this._localDisposableStore.dispose(); super.dispose(); + + // un-ref + this._previousFocusedElements = []; + this._viewModel = null; + this._hiddenRangeIds = []; + this.hiddenRangesPrefixSum = null; + this._visibleRanges = []; } } 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 dc74fc1fed..adbffdec03 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts @@ -4,82 +4,70 @@ *--------------------------------------------------------------------------------------------*/ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; +import { OutputRendererRegistry } from 'vs/workbench/contrib/notebook/browser/view/output/rendererRegistry'; import { onUnexpectedError } from 'vs/base/common/errors'; import { ICellOutputViewModel, ICommonNotebookEditor, IOutputTransformContribution, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { URI } from 'vs/base/common/uri'; +import { dispose } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; export class OutputRenderer { - protected readonly _contributions: { [key: string]: IOutputTransformContribution; }; - protected readonly _renderers: IOutputTransformContribution[]; - private _richMimeTypeRenderers = new Map(); + + private readonly _richMimeTypeRenderers = new Map(); constructor( notebookEditor: ICommonNotebookEditor, - private readonly instantiationService: IInstantiationService + instantiationService: IInstantiationService ) { - this._contributions = {}; - this._renderers = []; - - const contributions = NotebookRegistry.getOutputTransformContributions(); - - for (const desc of contributions) { + for (const desc of OutputRendererRegistry.getOutputTransformContributions()) { try { - const contribution = this.instantiationService.createInstance(desc.ctor, notebookEditor); - this._contributions[desc.id] = contribution; - contribution.getMimetypes().forEach(mimetype => { - this._richMimeTypeRenderers.set(mimetype, contribution); - }); + const contribution = instantiationService.createInstance(desc.ctor, notebookEditor); + contribution.getMimetypes().forEach(mimetype => { this._richMimeTypeRenderers.set(mimetype, contribution); }); } catch (err) { onUnexpectedError(err); } } } - getContribution(preferredMimeType: string | undefined): IOutputTransformContribution | undefined { - if (preferredMimeType) { - return this._richMimeTypeRenderers.get(preferredMimeType); - } - - return undefined; + dispose(): void { + dispose(this._richMimeTypeRenderers.values()); + this._richMimeTypeRenderers.clear(); } - renderNoop(viewModel: ICellOutputViewModel, container: HTMLElement): IRenderOutput { - const contentNode = document.createElement('p'); + getContribution(preferredMimeType: string): IOutputTransformContribution | undefined { + return this._richMimeTypeRenderers.get(preferredMimeType); + } - contentNode.innerText = `No renderer could be found for output.`; + private _renderMessage(container: HTMLElement, message: string): IRenderOutput { + const contentNode = document.createElement('p'); + contentNode.innerText = message; container.appendChild(contentNode); return { type: RenderOutputType.Mainframe }; } - render(viewModel: ICellOutputViewModel, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI | undefined): IRenderOutput { + render(viewModel: ICellOutputViewModel, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI): IRenderOutput { if (!viewModel.model.outputs.length) { - return this.renderNoop(viewModel, container); + return this._renderMessage(container, localize('empty', "Cell has no output")); } - - if (!preferredMimeType || !this._richMimeTypeRenderers.has(preferredMimeType)) { - const contentNode = document.createElement('p'); + if (!preferredMimeType) { const mimeTypes = viewModel.model.outputs.map(op => op.mime); - const mimeTypesMessage = mimeTypes.join(', '); - + return this._renderMessage(container, localize('noRenderer.2', "No renderer could be found for output. It has the following MIME types: {0}", mimeTypesMessage)); + } + if (!preferredMimeType || !this._richMimeTypeRenderers.has(preferredMimeType)) { if (preferredMimeType) { - contentNode.innerText = `No renderer could be found for MIME type: ${preferredMimeType}`; - } else { - contentNode.innerText = `No renderer could be found for output. It has the following MIME types: ${mimeTypesMessage}`; + return this._renderMessage(container, localize('noRenderer.1', "No renderer could be found for MIME type: {0}", preferredMimeType)); } - - container.appendChild(contentNode); - return { type: RenderOutputType.Mainframe }; } - const renderer = this._richMimeTypeRenderers.get(preferredMimeType); - const items = viewModel.model.outputs.filter(op => op.mime === preferredMimeType); - - if (items.length && renderer) { - return renderer.render(viewModel, items, container, notebookUri); - } else { - return this.renderNoop(viewModel, container); + if (!renderer) { + return this._renderMessage(container, localize('noRenderer.1', "No renderer could be found for MIME type: {0}", preferredMimeType)); } + const first = viewModel.model.outputs.find(op => op.mime === preferredMimeType); + if (!first) { + return this._renderMessage(container, localize('empty', "Cell has no output")); + } + + return renderer.render(viewModel, first, container, notebookUri); } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts b/src/vs/workbench/contrib/notebook/browser/view/output/rendererRegistry.ts similarity index 66% rename from src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts rename to src/vs/workbench/contrib/notebook/browser/view/output/rendererRegistry.ts index 7370fb72bf..c915991b08 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/rendererRegistry.ts @@ -9,20 +9,18 @@ import { ICommonNotebookEditor, IOutputTransformContribution } from 'vs/workbenc export type IOutputTransformCtor = IConstructorSignature1; export interface IOutputTransformDescription { - id: string; ctor: IOutputTransformCtor; } +export const OutputRendererRegistry = new class NotebookRegistryImpl { -export const NotebookRegistry = new class NotebookRegistryImpl { + readonly #outputTransforms: IOutputTransformDescription[] = []; - readonly outputTransforms: IOutputTransformDescription[] = []; - - registerOutputTransform(id: string, ctor: { new(editor: ICommonNotebookEditor, ...services: Services): IOutputTransformContribution }): void { - this.outputTransforms.push({ id: id, ctor: ctor as IOutputTransformCtor }); + registerOutputTransform(ctor: { new(editor: ICommonNotebookEditor, ...services: Services): IOutputTransformContribution }): void { + this.#outputTransforms.push({ ctor: ctor as IOutputTransformCtor }); } getOutputTransformContributions(): IOutputTransformDescription[] { - return this.outputTransforms.slice(0); + return this.#outputTransforms.slice(0); } }; 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 a3ff564adf..e140a59f83 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 @@ -4,81 +4,25 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { dirname } from 'vs/base/common/resources'; -import { isArray } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; +import { IEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { handleANSIOutput } from 'vs/workbench/contrib/debug/browser/debugANSIHandling'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; import { ICellOutputViewModel, ICommonNotebookEditor, IOutputTransformContribution as IOutputRendererContribution, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; +import { OutputRendererRegistry } from 'vs/workbench/contrib/notebook/browser/view/output/rendererRegistry'; import { truncatedArrayOfString } from 'vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper'; import { IOutputItemDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -function getStringValue(data: unknown): string { - return isArray(data) ? data.join('') : String(data); -} - -class JSONRendererContrib extends Disposable implements IOutputRendererContribution { - getType() { - return RenderOutputType.Mainframe; - } - - getMimetypes() { - return ['application/json']; - } - - constructor( - public notebookEditor: ICommonNotebookEditor, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IModelService private readonly modelService: IModelService, - @IModeService private readonly modeService: IModeService, - ) { - super(); - } - - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput { - const str = items.map(item => JSON.stringify(item.value, null, '\t')).join(''); - - const editor = this.instantiationService.createInstance(CodeEditorWidget, container, { - ...getOutputSimpleEditorOptions(), - dimension: { - width: 0, - height: 0 - }, - automaticLayout: true, - }, { - isSimpleWidget: true - }); - - const mode = this.modeService.create('json'); - const resource = URI.parse(`notebook-output-${Date.now()}.json`); - const textModel = this.modelService.createModel(str, mode, resource, false); - editor.setModel(textModel); - - 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({ - height, - width - }); - - container.style.height = `${height + 8}px`; - - return { type: RenderOutputType.Mainframe, initHeight: height }; - } -} class JavaScriptRendererContrib extends Disposable implements IOutputRendererContribution { getType() { @@ -95,14 +39,11 @@ class JavaScriptRendererContrib extends Disposable implements IOutputRendererCon super(); } - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput { - let scriptVal = ''; - items.forEach(item => { - const data = item.value; - const str = isArray(data) ? data.join('') : data; - scriptVal += ``; + render(output: ICellOutputViewModel, item: IOutputItemDto, container: HTMLElement, notebookUri: URI): IRenderOutput { + + const str = getStringValue(item); + const scriptVal = ``; - }); return { type: RenderOutputType.Html, source: output, @@ -129,35 +70,43 @@ class CodeRendererContrib extends Disposable implements IOutputRendererContribut super(); } - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput { - const str = items.map(item => getStringValue(item.value)).join(''); - const editor = this.instantiationService.createInstance(CodeEditorWidget, container, { - ...getOutputSimpleEditorOptions(), - dimension: { - width: 0, - height: 0 - } - }, { - isSimpleWidget: true - }); + render(output: ICellOutputViewModel, item: IOutputItemDto, container: HTMLElement): IRenderOutput { + const value = getStringValue(item); + return this._render(output, container, value, 'javascript'); + } - const mode = this.modeService.create('javascript'); - const resource = URI.parse(`notebook-output-${Date.now()}.js`); - const textModel = this.modelService.createModel(str, mode, resource, false); + protected _render(output: ICellOutputViewModel, container: HTMLElement, value: string, modeId: string): IRenderOutput { + const disposable = new DisposableStore(); + const editor = this.instantiationService.createInstance(CodeEditorWidget, container, getOutputSimpleEditorOptions(), { isSimpleWidget: true }); + + const mode = this.modeService.create(modeId); + const textModel = this.modelService.createModel(value, mode, undefined, false); editor.setModel(textModel); 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({ - height, - width - }); + editor.layout({ height, width }); + + disposable.add(editor); + disposable.add(textModel); container.style.height = `${height + 8}px`; - return { type: RenderOutputType.Mainframe }; + return { type: RenderOutputType.Mainframe, initHeight: height, disposable }; + } +} + +class JSONRendererContrib extends CodeRendererContrib { + + override getMimetypes() { + return ['application/json']; + } + + override render(output: ICellOutputViewModel, item: IOutputItemDto, container: HTMLElement): IRenderOutput { + const str = getStringValue(item); + return this._render(output, container, str, 'jsonc'); } } @@ -167,28 +116,25 @@ class StreamRendererContrib extends Disposable implements IOutputRendererContrib } getMimetypes() { - return ['application/x.notebook.stdout', 'application/x.notebook.stream']; + return ['application/vnd.code.notebook.stdout', 'application/x.notebook.stdout', 'application/x.notebook.stream']; } constructor( public notebookEditor: ICommonNotebookEditor, @IOpenerService private readonly openerService: IOpenerService, @IThemeService private readonly themeService: IThemeService, - @ITextFileService private readonly textFileService: ITextFileService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); } - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput { + render(output: ICellOutputViewModel, item: IOutputItemDto, container: HTMLElement, notebookUri: URI): IRenderOutput { const linkDetector = this.instantiationService.createInstance(LinkDetector); - items.forEach(item => { - const text = getStringValue(item.value); - const contentNode = DOM.$('span.output-stream'); - truncatedArrayOfString(contentNode, [text], linkDetector, this.openerService, this.textFileService, this.themeService); - container.appendChild(contentNode); - }); + const text = getStringValue(item); + const contentNode = DOM.$('span.output-stream'); + truncatedArrayOfString(notebookUri, output.cellViewModel, contentNode, [text], linkDetector, this.openerService, this.themeService); + container.appendChild(contentNode); return { type: RenderOutputType.Mainframe }; } @@ -200,64 +146,67 @@ class StderrRendererContrib extends StreamRendererContrib { } override getMimetypes() { - return ['application/x.notebook.stderr']; + return ['application/vnd.code.notebook.stderr', 'application/x.notebook.stderr']; } - override render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput { - const result = super.render(output, items, container, notebookUri); + override render(output: ICellOutputViewModel, item: IOutputItemDto, container: HTMLElement, notebookUri: URI): IRenderOutput { + const result = super.render(output, item, container, notebookUri); container.classList.add('error'); return result; } } -class ErrorRendererContrib extends Disposable implements IOutputRendererContribution { +class JSErrorRendererContrib implements IOutputRendererContribution { + + constructor( + public notebookEditor: ICommonNotebookEditor, + @IThemeService private readonly _themeService: IThemeService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ILogService private readonly _logService: ILogService, + ) { } + + dispose(): void { + // nothing + } + getType() { return RenderOutputType.Mainframe; } getMimetypes() { - return ['application/x.notebook.error-traceback']; + return ['application/vnd.code.notebook.error']; } - constructor( - public notebookEditor: ICommonNotebookEditor, - @IThemeService private readonly themeService: IThemeService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + render(_output: ICellOutputViewModel, item: IOutputItemDto, container: HTMLElement, _notebookUri: URI): IRenderOutput { + const linkDetector = this._instantiationService.createInstance(LinkDetector); - ) { - super(); - } + type ErrorLike = Partial; - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput { - const linkDetector = this.instantiationService.createInstance(LinkDetector); - items.forEach(item => { - const data: any = item.value; - const header = document.createElement('div'); - const headerMessage = data.ename && data.evalue - ? `${data.ename}: ${data.evalue}` - : data.ename || data.evalue; - if (headerMessage) { - header.innerText = headerMessage; - container.appendChild(header); - } - const traceback = document.createElement('pre'); - traceback.classList.add('traceback'); - if (data.traceback) { - for (let j = 0; j < data.traceback.length; j++) { - traceback.appendChild(handleANSIOutput(data.traceback[j], linkDetector, this.themeService, undefined)); - } - } - container.appendChild(traceback); - container.classList.add('error'); + + let err: ErrorLike; + try { + err = JSON.parse(getStringValue(item)); + } catch (e) { + this._logService.warn('INVALID output item (failed to parse)', e); return { type: RenderOutputType.Mainframe }; + } - }); + const header = document.createElement('div'); + const headerMessage = err.name && err.message ? `${err.name}: ${err.message}` : err.name || err.message; + if (headerMessage) { + header.innerText = headerMessage; + container.appendChild(header); + } + const stack = document.createElement('pre'); + stack.classList.add('traceback'); + if (err.stack) { + stack.appendChild(handleANSIOutput(err.stack, linkDetector, this._themeService, undefined)); + } + container.appendChild(stack); + container.classList.add('error'); return { type: RenderOutputType.Mainframe }; } - - _render() { - } } class PlainTextRendererContrib extends Disposable implements IOutputRendererContribution { @@ -273,18 +222,17 @@ class PlainTextRendererContrib extends Disposable implements IOutputRendererCont public notebookEditor: ICommonNotebookEditor, @IOpenerService private readonly openerService: IOpenerService, @IThemeService private readonly themeService: IThemeService, - @ITextFileService private readonly textFileService: ITextFileService, @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); } - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput { + render(output: ICellOutputViewModel, item: IOutputItemDto, container: HTMLElement, notebookUri: URI): IRenderOutput { const linkDetector = this.instantiationService.createInstance(LinkDetector); - const str = items.map(item => getStringValue(item.value)); + const str = getStringValue(item); const contentNode = DOM.$('.output-plaintext'); - truncatedArrayOfString(contentNode, str, linkDetector, this.openerService, this.textFileService, this.themeService); + truncatedArrayOfString(notebookUri, output.cellViewModel, contentNode, [str], linkDetector, this.openerService, this.themeService); container.appendChild(contentNode); return { type: RenderOutputType.Mainframe, supportAppend: true }; @@ -297,7 +245,7 @@ class HTMLRendererContrib extends Disposable implements IOutputRendererContribut } getMimetypes() { - return ['text/html']; + return ['text/html', 'image/svg+xml']; } constructor( @@ -306,35 +254,8 @@ class HTMLRendererContrib extends Disposable implements IOutputRendererContribut super(); } - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput { - const data = items.map(item => getStringValue(item.value)).join(''); - - const str = (isArray(data) ? data.join('') : data) as string; - return { - type: RenderOutputType.Html, - source: output, - htmlContent: str - }; - } -} - -class SVGRendererContrib extends Disposable implements IOutputRendererContribution { - getType() { - return RenderOutputType.Html; - } - - getMimetypes() { - return ['image/svg+xml']; - } - - constructor( - public notebookEditor: ICommonNotebookEditor, - ) { - super(); - } - - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput { - const str = items.map(item => getStringValue(item.value)).join(''); + render(output: ICellOutputViewModel, item: IOutputItemDto, container: HTMLElement, notebookUri: URI): IRenderOutput { + const str = getStringValue(item); return { type: RenderOutputType.Html, source: output, @@ -359,27 +280,25 @@ class MdRendererContrib extends Disposable implements IOutputRendererContributio super(); } - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI): IRenderOutput { - items.forEach(item => { - const data = item.value; - const str = (isArray(data) ? data.join('') : data) as string; - const mdOutput = document.createElement('div'); - const mdRenderer = this.instantiationService.createInstance(MarkdownRenderer, { baseUrl: dirname(notebookUri) }); - mdOutput.appendChild(mdRenderer.render({ value: str, isTrusted: true, supportThemeIcons: true }, undefined, { gfm: true }).element); - container.appendChild(mdOutput); - }); - - return { type: RenderOutputType.Mainframe }; + render(output: ICellOutputViewModel, item: IOutputItemDto, container: HTMLElement, notebookUri: URI): IRenderOutput { + const disposable = new DisposableStore(); + const str = getStringValue(item); + const mdOutput = document.createElement('div'); + const mdRenderer = this.instantiationService.createInstance(MarkdownRenderer, { baseUrl: dirname(notebookUri) }); + mdOutput.appendChild(mdRenderer.render({ value: str, isTrusted: true, supportThemeIcons: true }, undefined, { gfm: true }).element); + container.appendChild(mdOutput); + disposable.add(mdRenderer); + return { type: RenderOutputType.Mainframe, disposable }; } } -class PNGRendererContrib extends Disposable implements IOutputRendererContribution { +class ImgRendererContrib extends Disposable implements IOutputRendererContribution { getType() { return RenderOutputType.Mainframe; } getMimetypes() { - return ['image/png']; + return ['image/png', 'image/jpeg', 'image/gif']; } constructor( @@ -388,65 +307,45 @@ class PNGRendererContrib extends Disposable implements IOutputRendererContributi super(); } - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput { - items.forEach(item => { - const image = document.createElement('img'); - const imagedata = item.value; - image.src = `data:image/png;base64,${imagedata}`; - const display = document.createElement('div'); - display.classList.add('display'); - display.appendChild(image); - container.appendChild(display); - }); - return { type: RenderOutputType.Mainframe }; + render(output: ICellOutputViewModel, item: IOutputItemDto, container: HTMLElement, notebookUri: URI): IRenderOutput { + const disposable = new DisposableStore(); + + const bytes = new Uint8Array(item.valueBytes); + const blob = new Blob([bytes], { type: item.mime }); + const src = URL.createObjectURL(blob); + disposable.add(toDisposable(() => URL.revokeObjectURL(src))); + + const image = document.createElement('img'); + image.src = src; + const display = document.createElement('div'); + display.classList.add('display'); + display.appendChild(image); + container.appendChild(display); + + return { type: RenderOutputType.Mainframe, disposable }; } } -class JPEGRendererContrib extends Disposable implements IOutputRendererContribution { - getType() { - return RenderOutputType.Mainframe; - } +OutputRendererRegistry.registerOutputTransform(JSONRendererContrib); +OutputRendererRegistry.registerOutputTransform(JavaScriptRendererContrib); +OutputRendererRegistry.registerOutputTransform(HTMLRendererContrib); +OutputRendererRegistry.registerOutputTransform(MdRendererContrib); +OutputRendererRegistry.registerOutputTransform(ImgRendererContrib); +OutputRendererRegistry.registerOutputTransform(PlainTextRendererContrib); +OutputRendererRegistry.registerOutputTransform(CodeRendererContrib); +OutputRendererRegistry.registerOutputTransform(JSErrorRendererContrib); +OutputRendererRegistry.registerOutputTransform(StreamRendererContrib); +OutputRendererRegistry.registerOutputTransform(StderrRendererContrib); - getMimetypes() { - return ['image/jpeg']; - } - - constructor( - public notebookEditor: ICommonNotebookEditor, - ) { - super(); - } - - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput { - items.forEach(item => { - const image = document.createElement('img'); - const imagedata = item.value; - image.src = `data:image/jpeg;base64,${imagedata}`; - const display = document.createElement('div'); - display.classList.add('display'); - display.appendChild(image); - container.appendChild(display); - }); - - return { type: RenderOutputType.Mainframe }; - } +// --- utils --- +function getStringValue(item: IOutputItemDto): string { + // todo@jrieken NOT proper, should be VSBuffer + return new TextDecoder().decode(new Uint8Array(item.valueBytes)); } -NotebookRegistry.registerOutputTransform('json', JSONRendererContrib); -NotebookRegistry.registerOutputTransform('javascript', JavaScriptRendererContrib); -NotebookRegistry.registerOutputTransform('html', HTMLRendererContrib); -NotebookRegistry.registerOutputTransform('svg', SVGRendererContrib); -NotebookRegistry.registerOutputTransform('markdown', MdRendererContrib); -NotebookRegistry.registerOutputTransform('png', PNGRendererContrib); -NotebookRegistry.registerOutputTransform('jpeg', JPEGRendererContrib); -NotebookRegistry.registerOutputTransform('plain', PlainTextRendererContrib); -NotebookRegistry.registerOutputTransform('code', CodeRendererContrib); -NotebookRegistry.registerOutputTransform('error-trace', ErrorRendererContrib); -NotebookRegistry.registerOutputTransform('stream-text', StreamRendererContrib); -NotebookRegistry.registerOutputTransform('stderr', StderrRendererContrib); - -export function getOutputSimpleEditorOptions(): IEditorOptions { +function getOutputSimpleEditorOptions(): IEditorConstructionOptions { return { + dimension: { height: 0, width: 0 }, readOnly: true, wordWrap: 'on', overviewRulerLanes: 0, @@ -464,6 +363,7 @@ export function getOutputSimpleEditorOptions(): IEditorOptions { lineNumbers: 'off', scrollbar: { alwaysConsumeMouseWheel: false - } + }, + automaticLayout: true, }; } 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 bc159076e6..d275a7adcc 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 @@ -12,14 +12,17 @@ import { Range } from 'vs/editor/common/core/range'; import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { handleANSIOutput } from 'vs/workbench/contrib/debug/browser/debugANSIHandling'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; +import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; +import { IGenericCellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; const SIZE_LIMIT = 65535; const LINES_LIMIT = 500; -function generateViewMoreElement(outputs: string[], openerService: IOpenerService, textFileService: ITextFileService) { +function generateViewMoreElement(notebookUri: URI, cellViewModel: IGenericCellViewModel, outputs: string[], openerService: IOpenerService) { const md: IMarkdownString = { value: '[show more (open the raw output data in a text editor) ...](command:workbench.action.openLargeOutput)', isTrusted: true, @@ -30,14 +33,7 @@ function generateViewMoreElement(outputs: string[], openerService: IOpenerServic actionHandler: { callback: (content) => { if (content === 'command:workbench.action.openLargeOutput') { - return textFileService.untitled.resolve({ - associatedResource: undefined, - mode: 'plaintext', - initialValue: outputs.join('') - }).then(model => { - const resource = model.resource; - openerService.open(resource); - }); + openerService.open(CellUri.generateCellUri(notebookUri, cellViewModel.handle, Schemas.vscodeNotebookCellOutput)); } return undefined; // {{SQL CARBON EDIT}} @@ -50,7 +46,7 @@ function generateViewMoreElement(outputs: string[], openerService: IOpenerServic return element; } -export function truncatedArrayOfString(container: HTMLElement, outputs: string[], linkDetector: LinkDetector, openerService: IOpenerService, textFileService: ITextFileService, themeService: IThemeService) { +export function truncatedArrayOfString(notebookUri: URI, cellViewModel: IGenericCellViewModel, container: HTMLElement, outputs: string[], linkDetector: LinkDetector, openerService: IOpenerService, themeService: IThemeService) { const fullLen = outputs.reduce((p, c) => { return p + c.length; }, 0); @@ -68,7 +64,7 @@ export function truncatedArrayOfString(container: HTMLElement, outputs: string[] const truncatedText = buffer.getValueInRange(new Range(1, 1, sizeBufferLimitPosition.lineNumber, sizeBufferLimitPosition.column), EndOfLinePreference.TextDefined); container.appendChild(handleANSIOutput(truncatedText, linkDetector, themeService, undefined)); // view more ... - container.appendChild(generateViewMoreElement(outputs, openerService, textFileService)); + container.appendChild(generateViewMoreElement(notebookUri, cellViewModel, outputs, openerService)); return; } } @@ -92,7 +88,7 @@ export function truncatedArrayOfString(container: HTMLElement, outputs: string[] pre.appendChild(handleANSIOutput(buffer.getValueInRange(new Range(1, 1, LINES_LIMIT - 5, buffer.getLineLastNonWhitespaceColumn(LINES_LIMIT - 5)), EndOfLinePreference.TextDefined), linkDetector, themeService, undefined)); // view more ... - container.appendChild(generateViewMoreElement(outputs, openerService, textFileService)); + container.appendChild(generateViewMoreElement(notebookUri, cellViewModel, outputs, openerService)); const lineCount = buffer.getLineCount(); const pre2 = DOM.$('div'); 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 9f4a2e0527..1b33a596a2 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -24,357 +24,17 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { asWebviewUri } from 'vs/workbench/api/common/shared/webview'; import { CellEditState, ICellOutputViewModel, 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 { preloadsScriptStr, RendererMetadata } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads'; import { transformWebviewThemeVars } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; -import { INotebookRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookKernel, INotebookRendererInfo, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IScopedRendererMessaging } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; 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'; - -interface BaseToWebviewMessage { - readonly __vscode_notebook_message: true; -} - -export interface WebviewIntialized extends BaseToWebviewMessage { - type: 'initialized'; -} - -export interface DimensionUpdate { - id: string; - init?: boolean; - height: number; - isOutput?: boolean; -} - -export interface IDimensionMessage extends BaseToWebviewMessage { - type: 'dimension'; - updates: readonly DimensionUpdate[]; -} - -export interface IMouseEnterMessage extends BaseToWebviewMessage { - type: 'mouseenter'; - id: string; -} - -export interface IMouseLeaveMessage extends BaseToWebviewMessage { - type: 'mouseleave'; - id: string; -} - -export interface IOutputFocusMessage extends BaseToWebviewMessage { - type: 'outputFocus'; - id: string; -} - -export interface IOutputBlurMessage extends BaseToWebviewMessage { - type: 'outputBlur'; - id: string; -} - -export interface IWheelMessage extends BaseToWebviewMessage { - type: 'did-scroll-wheel'; - payload: any; -} - -export interface IScrollAckMessage extends BaseToWebviewMessage { - type: 'scroll-ack'; - data: { top: number }; - version: number; -} - -export interface IBlurOutputMessage extends BaseToWebviewMessage { - type: 'focus-editor'; - id: string; - focusNext?: boolean; -} - -export interface IClickedDataUrlMessage extends BaseToWebviewMessage { - type: 'clicked-data-url'; - data: string | ArrayBuffer | null; - downloadName?: string; -} - -export interface IClickMarkdownPreviewMessage extends BaseToWebviewMessage { - readonly type: 'clickMarkdownPreview'; - readonly cellId: string; - readonly ctrlKey: boolean - readonly altKey: boolean; - readonly metaKey: boolean; - readonly shiftKey: boolean; -} - -export interface IContextMenuMarkdownPreviewMessage extends BaseToWebviewMessage { - readonly type: 'contextMenuMarkdownPreview'; - readonly cellId: string; - readonly clientX: number; - readonly clientY: number; -} - -export interface IMouseEnterMarkdownPreviewMessage extends BaseToWebviewMessage { - type: 'mouseEnterMarkdownPreview'; - cellId: string; -} - -export interface IMouseLeaveMarkdownPreviewMessage extends BaseToWebviewMessage { - type: 'mouseLeaveMarkdownPreview'; - cellId: string; -} - -export interface IToggleMarkdownPreviewMessage extends BaseToWebviewMessage { - type: 'toggleMarkdownPreview'; - cellId: string; -} - -export interface ICellDragStartMessage extends BaseToWebviewMessage { - type: 'cell-drag-start'; - readonly cellId: string; - readonly position: { - readonly clientY: number; - }; -} - -export interface ICellDragMessage extends BaseToWebviewMessage { - type: 'cell-drag'; - readonly cellId: string; - readonly position: { - readonly clientY: number; - }; -} - -export interface ICellDropMessage extends BaseToWebviewMessage { - readonly type: 'cell-drop'; - readonly cellId: string; - readonly ctrlKey: boolean - readonly altKey: boolean; - readonly position: { - readonly clientY: number; - }; -} - -export interface ICellDragEndMessage extends BaseToWebviewMessage { - readonly type: 'cell-drag-end'; - readonly cellId: string; -} - -export interface IInitializedMarkdownPreviewMessage extends BaseToWebviewMessage { - readonly type: 'initializedMarkdownPreview'; -} - -export interface ITelemetryFoundRenderedMarkdownMath extends BaseToWebviewMessage { - readonly type: 'telemetryFoundRenderedMarkdownMath'; -} - -export interface ITelemetryFoundUnrenderedMarkdownMath extends BaseToWebviewMessage { - readonly type: 'telemetryFoundUnrenderedMarkdownMath'; - readonly latexDirective: string; -} - -export interface IClearMessage { - type: 'clear'; -} - -export interface IOutputRequestMetadata { - /** - * Additional attributes of a cell metadata. - */ - custom?: { [key: string]: unknown }; -} - -export interface IOutputRequestDto { - /** - * { mime_type: value } - */ - data: { [key: string]: unknown; } - - metadata?: IOutputRequestMetadata; - outputId: string; -} - -export interface ICreationRequestMessage { - type: 'html'; - content: - | { type: RenderOutputType.Html; htmlContent: string } - | { type: RenderOutputType.Extension; outputId: string; value: unknown; metadata: unknown; mimeType: string }; - cellId: string; - outputId: string; - cellTop: number; - outputOffset: number; - left: number; - requiredPreloads: ReadonlyArray; - readonly initiallyHidden?: boolean; - apiNamespace?: string | undefined; -} - -export interface IContentWidgetTopRequest { - outputId: string; - cellTop: number; - outputOffset: number; - forceDisplay: boolean; -} - -export interface IViewScrollTopRequestMessage { - type: 'view-scroll'; - widgets: IContentWidgetTopRequest[]; - markdownPreviews: { id: string; top: number }[]; -} - -export interface IScrollRequestMessage { - type: 'scroll'; - id: string; - top: number; - widgetTop?: number; - version: number; -} - -export interface IClearOutputRequestMessage { - type: 'clearOutput'; - cellId: string; - outputId: string; - cellUri: string; - apiNamespace: string | undefined; -} - -export interface IHideOutputMessage { - type: 'hideOutput'; - outputId: string; - cellId: string; -} - -export interface IShowOutputMessage { - type: 'showOutput'; - cellId: string; - outputId: string; - cellTop: number; - outputOffset: number; -} - -export interface IFocusOutputMessage { - type: 'focus-output'; - cellId: string; -} - -export interface IAckOutputHeightMessage { - type: 'ack-dimension', - cellId: string; - outputId: string; - height: number; -} - -export interface IPreloadResource { - originalUri: string; - uri: string; -} - -export interface IUpdatePreloadResourceMessage { - type: 'preload'; - resources: IPreloadResource[]; - source: 'renderer' | 'kernel'; -} - -export interface IUpdateDecorationsMessage { - type: 'decorations'; - cellId: string; - addedClassNames: string[]; - removedClassNames: string[]; -} - -export interface ICustomRendererMessage extends BaseToWebviewMessage { - type: 'customRendererMessage'; - rendererId: string; - message: unknown; -} - -export interface ICreateMarkdownMessage { - type: 'createMarkdownPreview', - id: string; - handle: number; - content: string; - top: number; -} -export interface IDeleteMarkdownMessage { - type: 'deleteMarkdownPreview', - ids: readonly string[]; -} - -export interface IHideMarkdownMessage { - type: 'hideMarkdownPreviews'; - ids: readonly string[]; -} - -export interface IUnhideMarkdownMessage { - type: 'unhideMarkdownPreviews'; - ids: readonly string[]; -} - -export interface IShowMarkdownMessage { - type: 'showMarkdownPreview', - id: string; - handle: number; - content: string | undefined; - top: number; -} - -export interface IUpdateSelectedMarkdownPreviews { - readonly type: 'updateSelectedMarkdownPreviews', - readonly selectedCellIds: readonly string[] -} - -export interface IInitializeMarkdownMessage { - type: 'initializeMarkdownPreview'; - cells: Array<{ cellId: string, cellHandle: number, content: string, offset: number }>; -} - -export type FromWebviewMessage = - | WebviewIntialized - | IDimensionMessage - | IMouseEnterMessage - | IMouseLeaveMessage - | IOutputFocusMessage - | IOutputBlurMessage - | IWheelMessage - | IScrollAckMessage - | IBlurOutputMessage - | ICustomRendererMessage - | IClickedDataUrlMessage - | IClickMarkdownPreviewMessage - | IContextMenuMarkdownPreviewMessage - | IMouseEnterMarkdownPreviewMessage - | IMouseLeaveMarkdownPreviewMessage - | IToggleMarkdownPreviewMessage - | ICellDragStartMessage - | ICellDragMessage - | ICellDropMessage - | ICellDragEndMessage - | IInitializedMarkdownPreviewMessage - | ITelemetryFoundRenderedMarkdownMath - | ITelemetryFoundUnrenderedMarkdownMath - ; - -export type ToWebviewMessage = - | IClearMessage - | IFocusOutputMessage - | IAckOutputHeightMessage - | ICreationRequestMessage - | IViewScrollTopRequestMessage - | IScrollRequestMessage - | IClearOutputRequestMessage - | IHideOutputMessage - | IShowOutputMessage - | IUpdatePreloadResourceMessage - | IUpdateDecorationsMessage - | ICustomRendererMessage - | ICreateMarkdownMessage - | IDeleteMarkdownMessage - | IShowMarkdownMessage - | IHideMarkdownMessage - | IUnhideMarkdownMessage - | IUpdateSelectedMarkdownPreviews - | IInitializeMarkdownMessage; - -export type AnyMessage = FromWebviewMessage | ToWebviewMessage; +import { ICreationRequestMessage, IMarkupCellInitialization, FromWebviewMessage, IClickedDataUrlMessage, IContentWidgetTopRequest, IControllerPreload, ToWebviewMessage } from './webviewMessages'; export interface ICachedInset { outputId: string; @@ -393,7 +53,6 @@ function html(strings: TemplateStringsArray, ...values: any[]): string { export interface INotebookWebviewMessage { message: unknown; - forRenderer?: string; } export interface IResolvedBackLayerWebview { @@ -404,31 +63,34 @@ export class BackLayerWebView extends Disposable { element: HTMLElement; webview: WebviewElement | undefined = undefined; insetMapping: Map> = new Map(); - readonly markdownPreviewMapping = new Map(); + readonly markdownPreviewMapping = new Map(); hiddenInsetMapping: Set = new Set(); reversedInsetMapping: Map = new Map(); localResourceRootsCache: URI[] | undefined = undefined; rendererRootsCache: URI[] = []; - kernelRootsCache: URI[] = []; private readonly _onMessage = this._register(new Emitter()); private readonly _preloadsCache = new Set(); public readonly onMessage: Event = this._onMessage.event; - private _loaded!: Promise; private _initalized?: Promise; private _disposed = false; + private _currentKernel?: INotebookKernel; constructor( - public notebookEditor: ICommonNotebookEditor, - public id: string, - public documentUri: URI, - public options: { + public readonly notebookEditor: ICommonNotebookEditor, + public readonly id: string, + public readonly documentUri: URI, + private options: { outputNodePadding: number, outputNodeLeftPadding: number, previewNodePadding: number, + markdownLeftMargin: number, leftMargin: number, rightMargin: number, runGutter: number, + dragAndDropEnabled: boolean, + fontSize: number }, + private readonly rendererMessaging: IScopedRendererMessaging | undefined, @IWebviewService readonly webviewService: IWebviewService, @IOpenerService readonly openerService: IOpenerService, @INotebookService private readonly notebookService: INotebookService, @@ -447,158 +109,73 @@ export class BackLayerWebView extends Disposable { this.element.style.height = '1400px'; this.element.style.position = 'absolute'; + + if (rendererMessaging) { + this._register(rendererMessaging.onDidReceiveMessage(evt => { + this._sendMessageToWebview({ + __vscode_notebook_message: true, + type: 'customRendererMessage', + rendererId: evt.rendererId, + message: evt.message + }); + })); + } } + + updateOptions(options: { + outputNodePadding: number, + outputNodeLeftPadding: number, + previewNodePadding: number, + markdownLeftMargin: number, + leftMargin: number, + rightMargin: number, + runGutter: number, + dragAndDropEnabled: boolean, + fontSize: number + }) { + this.options = options; + this._updateStyles(); + this._updateOptions(); + } + + private _updateStyles() { + this._sendMessageToWebview({ + type: 'notebookStyles', + styles: this._generateStyles() + }); + } + + private _updateOptions() { + this._sendMessageToWebview({ + type: 'notebookOptions', + options: { + dragAndDropEnabled: this.options.dragAndDropEnabled + } + }); + } + + private _generateStyles() { + return { + 'notebook-output-left-margin': `${this.options.leftMargin + this.options.runGutter}px`, + 'notebook-output-width': `calc(100% - ${this.options.leftMargin + this.options.rightMargin + this.options.runGutter}px)`, + 'notebook-output-node-padding': `${this.options.outputNodePadding}px`, + 'notebook-run-gutter': `${this.options.runGutter}px`, + 'notebook-preivew-node-padding': `${this.options.previewNodePadding}px`, + 'notebook-markdown-left-margin': `${this.options.markdownLeftMargin}px`, + 'notebook-output-node-left-padding': `${this.options.outputNodeLeftPadding}px`, + 'notebook-markdown-min-height': `${this.options.previewNodePadding * 2}px`, + 'notebook-cell-output-font-size': `${this.options.fontSize}px`, + 'notebook-cell-markup-empty-content': nls.localize('notebook.emptyMarkdownPlaceholder', "Empty markdown cell, double click or press enter to edit."), + }; + } + private generateContent(coreDependencies: string, baseUrl: string) { - const markupRenderer = this.getMarkdownRenderer(); - const outputWidth = `calc(100% - ${this.options.leftMargin + this.options.rightMargin + this.options.runGutter}px)`; - const outputMarginLeft = `${this.options.leftMargin + this.options.runGutter}px`; + const renderersData = this.getRendererData(); return html` - - - ${uriTranformedContent} + `; } @@ -570,28 +791,19 @@ export class GettingStartedPage extends EditorPane { const someStepsComplete = this.gettingStartedCategories.some(categry => categry.content.type === 'steps' && categry.content.stepsComplete); if (!someStepsComplete && !this.hasScrolledToFirstCategory) { - const fistContentBehaviour = - !this.storageService.get(lastSessionDateStorageKey, StorageScope.GLOBAL) // isNewUser ? - ? 'openToFirstCategory' - : await Promise.race([ - this.tasExperimentService?.getTreatment<'index' | 'openToFirstCategory'>('GettingStartedFirstContent'), - new Promise<'index'>(resolve => setTimeout(() => resolve('index'), 1000)), - ]); + const firstSessionDateString = this.storageService.get(firstSessionDateStorageKey, StorageScope.GLOBAL) || new Date().toUTCString(); + const daysSinceFirstSession = ((+new Date()) - (+new Date(firstSessionDateString))) / 1000 / 60 / 60 / 24; + const fistContentBehaviour = daysSinceFirstSession < 1 ? 'openToFirstCategory' : 'index'; - if (this.gettingStartedCategories.some(category => category.content.type === 'steps' && category.content.stepsComplete)) { - this.setSlide('categories'); - return; - } else { - if (fistContentBehaviour === 'openToFirstCategory') { - const first = this.gettingStartedCategories.find(category => category.content.type === 'steps'); - this.hasScrolledToFirstCategory = true; - if (first) { - this.currentCategory = first; - this.editorInput.selectedCategory = this.currentCategory?.id; - this.buildCategorySlide(this.editorInput.selectedCategory); - this.setSlide('details'); - return; - } + if (fistContentBehaviour === 'openToFirstCategory') { + const first = this.gettingStartedCategories.find(category => category.content.type === 'steps'); + this.hasScrolledToFirstCategory = true; + if (first) { + this.currentCategory = first; + this.editorInput.selectedCategory = this.currentCategory?.id; + this.buildCategorySlide(this.editorInput.selectedCategory); + this.setSlide('details'); + return; } } } @@ -674,7 +886,7 @@ export class GettingStartedPage extends EditorPane { $('button.button-link', { 'x-dispatch': 'selectCategory:' + entry.id, - title: entry.description + this.getKeybindingLabel(entry.content.command), + title: entry.description + ' ' + this.getKeybindingLabel(entry.content.command), }, this.iconWidgetFor(entry), $('span', {}, entry.title))); @@ -749,6 +961,8 @@ export class GettingStartedPage extends EditorPane { this.gettingStartedList?.layout(size); this.recentlyOpenedList?.layout(size); + this.layoutMarkdown?.(); + this.container.classList[size.height <= 600 ? 'add' : 'remove']('height-constrained'); this.container.classList[size.width <= 400 ? 'add' : 'remove']('width-constrained'); this.container.classList[size.width <= 800 ? 'add' : 'remove']('width-semi-constrained'); @@ -772,22 +986,26 @@ export class GettingStartedPage extends EditorPane { bar.setAttribute('aria-valuemin', '0'); bar.setAttribute('aria-valuenow', '' + numDone); bar.setAttribute('aria-valuemax', '' + numTotal); - const progress = Math.max((numDone / numTotal) * 100, 3); + const progress = (numDone / numTotal) * 100; bar.style.width = `${progress}%`; + + (element.parentElement as HTMLElement).classList[numDone === 0 ? 'add' : 'remove']('no-progress'); + if (numTotal === numDone) { - bar.title = `All steps complete!`; + bar.title = localize('gettingStarted.allStepsComplete', "All {0} steps complete!", numTotal); } else { - bar.title = `${numDone} of ${numTotal} steps complete`; + bar.title = localize('gettingStarted.someStepsComplete', "{0} of {1} steps complete", numDone, numTotal); } }); } - private async scrollToCategory(categoryID: string) { + private async scrollToCategory(categoryID: string, stepId?: string) { this.inProgressScroll = this.inProgressScroll.then(async () => { reset(this.stepsContent); this.editorInput.selectedCategory = categoryID; + this.editorInput.selectedStep = stepId; this.currentCategory = this.gettingStartedCategories.find(category => category.id === categoryID); this.buildCategorySlide(categoryID); this.setSlide('details'); @@ -845,6 +1063,10 @@ export class GettingStartedPage extends EditorPane { } this.openerService.open(command, { allowCommands: true }); + if (!isCommand && (node.href.startsWith('https://') || node.href.startsWith('http://'))) { + this.gettingStartedService.progressByEvent('onLink:' + node.href); + } + }, null, this.detailsPageDisposables); if (isCommand) { @@ -862,11 +1084,10 @@ export class GettingStartedPage extends EditorPane { if (typeof node === 'string') { append(p, renderFormattedText(node, { inline: true, renderCodeSegements: true })); } else { - const link = this.instantiationService.createInstance(Link, node); + const link = this.instantiationService.createInstance(Link, node, {}); append(p, link.el); this.detailsPageDisposables.add(link); - this.detailsPageDisposables.add(attachLinkStyler(link, this.themeService)); } } } @@ -931,14 +1152,26 @@ export class GettingStartedPage extends EditorPane { stepDescription); }); - const stepsContainer = $('.getting-started-detail-container', { 'role': 'list' }, ...categoryElements); + const showNextCategory = + this.gettingStartedCategories.find(_category => _category.id === category.next && _category.content.type === 'steps' && !_category.content.done); + + const stepsContainer = $( + '.getting-started-detail-container', { 'role': 'list' }, + ...categoryElements, + $('.done-next-container', {}, + $('button.button-link.all-done', { 'x-dispatch': 'allDone' }, $('span.codicon.codicon-check-all'), localize('allDone', "Mark Done")), + ...(showNextCategory + ? [$('button.button-link.next', { 'x-dispatch': 'nextSection' }, localize('nextOne', "Next Section"), $('span.codicon.codicon-arrow-small-right'))] + : []), + ) + ); this.detailsScrollbar = this._register(new DomScrollableElement(stepsContainer, { className: 'steps-container' })); const stepListComponent = this.detailsScrollbar.getDomNode(); reset(this.stepsContent, categoryDescriptorComponent, stepListComponent, this.stepMediaComponent); const toExpand = category.content.steps.find(step => !step.done) ?? category.content.steps[0]; - this.selectStep(selectedStep ?? toExpand.id, false); + this.selectStep(selectedStep ?? toExpand.id, !selectedStep, true); this.detailsScrollbar.scanDomNode(); this.detailsPageScrollbar?.scanDomNode(); @@ -962,6 +1195,7 @@ export class GettingStartedPage extends EditorPane { this.editorInput.selectedStep = undefined; this.selectStep(undefined); this.setSlide('categories'); + this.container.focus(); }); } @@ -983,7 +1217,7 @@ export class GettingStartedPage extends EditorPane { if (allSteps) { const toFind = this.editorInput.selectedStep ?? this.previousSelection; const selectedIndex = allSteps.findIndex(step => step.id === toFind); - if (allSteps[selectedIndex + 1]?.id) { this.selectStep(allSteps[selectedIndex + 1]?.id, true, false); } + if (allSteps[selectedIndex + 1]?.id) { this.selectStep(allSteps[selectedIndex + 1]?.id, false); } } } else { (document.activeElement?.nextElementSibling as HTMLElement)?.focus?.(); @@ -996,7 +1230,7 @@ export class GettingStartedPage extends EditorPane { if (allSteps) { const toFind = this.editorInput.selectedStep ?? this.previousSelection; const selectedIndex = allSteps.findIndex(step => step.id === toFind); - if (allSteps[selectedIndex - 1]?.id) { this.selectStep(allSteps[selectedIndex - 1]?.id, true, false); } + if (allSteps[selectedIndex - 1]?.id) { this.selectStep(allSteps[selectedIndex - 1]?.id, false); } } } else { (document.activeElement?.previousElementSibling as HTMLElement)?.focus?.(); @@ -1011,7 +1245,6 @@ export class GettingStartedPage extends EditorPane { this.container.querySelector('.gettingStartedSlideDetails')!.querySelectorAll('button').forEach(button => button.disabled = true); this.container.querySelector('.gettingStartedSlideCategories')!.querySelectorAll('button').forEach(button => button.disabled = false); this.container.querySelector('.gettingStartedSlideCategories')!.querySelectorAll('input').forEach(button => button.disabled = false); - this.container.focus(); } else { slideManager.classList.add('showDetails'); slideManager.classList.remove('showCategories'); @@ -1020,6 +1253,10 @@ export class GettingStartedPage extends EditorPane { this.container.querySelector('.gettingStartedSlideCategories')!.querySelectorAll('input').forEach(button => button.disabled = true); } } + + override focus() { + this.container.focus(); + } } export class GettingStartedInputSerializer implements IEditorInputSerializer { @@ -1052,6 +1289,8 @@ class GettingStartedIndexList extends Disposable { public itemCount: number; + private isDisposed = false; + constructor( title: string, klass: string, @@ -1083,7 +1322,12 @@ class GettingStartedIndexList extends Disposable { this._register(this.onDidChangeEntries(listener)); } - register(d: IDisposable) { this._register(d); } + register(d: IDisposable) { if (this.isDisposed) { d.dispose(); } else { this._register(d); } } + + override dispose() { + this.isDisposed = true; + super.dispose(); + } setLimit(limit: number) { this.limit = limit; @@ -1191,7 +1435,7 @@ registerThemingParticipant((theme, collector) => { if (link) { collector.addRule(`.monaco-workbench .part.editor > .content .gettingStartedContainer a:not(.codicon-close) { color: ${link}; }`); collector.addRule(`.monaco-workbench .part.editor > .content .gettingStartedContainer .button-link { color: ${link}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .gettingStartedContainer .button-link .scroll-button { color: ${link}; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .gettingStartedContainer .button-link .codicon { color: ${link}; }`); } const activeLink = theme.getColor(textLinkActiveForeground); if (activeLink) { diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedExtensionPoint.ts b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedExtensionPoint.ts index ae51f02cf0..50d55c4587 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedExtensionPoint.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedExtensionPoint.ts @@ -1,17 +1,19 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; import { IStartEntry, IWalkthrough } from 'vs/platform/extensions/common/extensions'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +const titleTranslated = localize('title', "Title"); + export const walkthroughsExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'walkthroughs', jsonSchema: { doNotSuggest: true, - description: localize('walkthroughs', "Contribute collections of steps to help users with your extension. Experimental, available in VS Code Insiders only."), + description: localize('walkthroughs', "Contribute walkthroughs to help users getting started with your extension."), type: 'array', items: { type: 'object', @@ -31,8 +33,7 @@ export const walkthroughsExtensionPoint = ExtensionsRegistry.registerExtensionPo description: localize('walkthroughs.description', "Description of walkthrough.") }, primary: { - type: 'boolean', - description: localize('walkthroughs.primary', "if this is a `primary` walkthrough, hinting if it should be opened on install of the extension. The first `primary` walkthough with a `when` condition matching the current context may be opened by core on install of the extension.") + deprecationMessage: localize('walkthroughs.primary.deprecated', "Deprecated. The first walkthrough with a satisfied when condition will be opened on install.") }, when: { type: 'string', @@ -46,11 +47,11 @@ export const walkthroughsExtensionPoint = ExtensionsRegistry.registerExtensionPo description: localize('walkthroughs.steps', "Steps to complete as part of this walkthrough."), items: { type: 'object', - required: ['id', 'title', 'description', 'media'], + required: ['id', 'title', 'media'], defaultSnippets: [{ body: { 'id': '$1', 'title': '$2', 'description': '$3', - 'doneOn': { 'command': '$5' }, + 'completionEvents': ['$5'], 'media': { 'path': '$6', 'type': '$7' } } }], @@ -65,10 +66,10 @@ export const walkthroughsExtensionPoint = ExtensionsRegistry.registerExtensionPo }, description: { type: 'string', - description: localize('walkthroughs.steps.description', "Description of step. Supports ``preformatted``, __italic__, and **bold** text. Use markdown-style links for commands or external links: [Title](command:myext.command), [Title](command:toSide:myext.command), or [Title](https://aka.ms). Links on their own line will be rendered as buttons.") + description: localize('walkthroughs.steps.description.interpolated', "Description of step. Supports ``preformatted``, __italic__, and **bold** text. Use markdown-style links for commands or external links: {0}, {1}, or {2}. Links on their own line will be rendered as buttons.", `[${titleTranslated}](command:myext.command)`, `[${titleTranslated}](command:toSide:myext.command)`, `[${titleTranslated}](https://aka.ms)`) }, button: { - deprecationMessage: localize('walkthroughs.steps.button.deprecated', "Deprecated. Use markdown links in the description instead, i.e. [Title](command:myext.command), [Title](command:toSide:myext.command), or [Title](https://aka.ms), "), + deprecationMessage: localize('walkthroughs.steps.button.deprecated.interpolated', "Deprecated. Use markdown links in the description instead, i.e. {0}, {1}, or {2}", `[${titleTranslated}](command:myext.command)`, `[${titleTranslated}](command:toSide:myext.command)`, `[${titleTranslated}](https://aka.ms)`), }, media: { type: 'object', @@ -76,10 +77,13 @@ export const walkthroughsExtensionPoint = ExtensionsRegistry.registerExtensionPo defaultSnippets: [{ 'body': { 'type': '$1', 'path': '$2' } }], oneOf: [ { - required: ['path', 'altText'], + required: ['image', 'altText'], additionalProperties: false, properties: { path: { + deprecationMessage: localize('pathDeprecated', "Deprecated. Please use `image` or `markdown` instead") + }, + image: { description: localize('walkthroughs.steps.media.image.path.string', "Path to an image - or object consisting of paths to light, dark, and hc images - relative to extension directory. Depending on context, the image will be displayed from 400px to 800px wide, with similar bounds on height. To support HIDPI displays, the image will be rendered at 1.5x scaling, for example a 900 physical pixels wide image will be displayed as 600 logical pixels wide."), oneOf: [ { @@ -111,10 +115,13 @@ export const walkthroughsExtensionPoint = ExtensionsRegistry.registerExtensionPo } } }, { - required: ['path'], + required: ['markdown'], additionalProperties: false, properties: { path: { + deprecationMessage: localize('pathDeprecated', "Deprecated. Please use `image` or `markdown` instead") + }, + markdown: { description: localize('walkthroughs.steps.media.markdown.path', "Path to the markdown document, relative to extension directory."), type: 'string', } @@ -122,8 +129,53 @@ export const walkthroughsExtensionPoint = ExtensionsRegistry.registerExtensionPo } ] }, + completionEvents: { + description: localize('walkthroughs.steps.completionEvents', "Events that should trigger this step to become checked off. If empty or not defined, the step will check off when any of the step's buttons or links are clicked; if the step has no buttons or links it will check on when it is selected."), + type: 'array', + items: { + type: 'string', + defaultSnippets: [ + { + label: 'onCommand', + description: localize('walkthroughs.steps.completionEvents.onCommand', 'Check off step when a given command is executed anywhere in VS Code.'), + body: 'onCommand:${1:commandId}' + }, + { + label: 'onLink', + description: localize('walkthroughs.steps.completionEvents.onLink', 'Check off step when a given link is opened via a Getting Started step.'), + body: 'onLink:${2:linkId}' + }, + { + label: 'onView', + description: localize('walkthroughs.steps.completionEvents.onView', 'Check off step when a given view is opened'), + body: 'onView:${2:viewId}' + }, + { + label: 'onSettingChanged', + description: localize('walkthroughs.steps.completionEvents.onSettingChanged', 'Check off step when a given setting is changed'), + body: 'onSettingChanged:${2:settingName}' + }, + { + label: 'onContext', + description: localize('walkthroughs.steps.completionEvents.onContext', 'Check off step when a context key expression is true.'), + body: 'onContext:${2:key}' + }, + { + label: 'extensionInstalled', + description: localize('walkthroughs.steps.completionEvents.extensionInstalled', 'Check off step when an extension with the given id is installed. If the extension is already installed, the step will start off checked.'), + body: 'extensionInstalled:${3:extensionId}' + }, + { + label: 'stepSelected', + description: localize('walkthroughs.steps.completionEvents.stepSelected', 'Check off step as soon as it is selected.'), + body: 'stepSelected' + }, + ] + } + }, doneOn: { description: localize('walkthroughs.steps.doneOn', "Signal to mark step as complete."), + deprecationMessage: localize('walkthroughs.steps.doneOn.deprecation', "doneOn is deprecated. By default steps will be checked off when their buttons are clicked, to configure further use completionEvents"), type: 'object', required: ['command'], defaultSnippets: [{ 'body': { command: '$1' } }], diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedInput.ts b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedInput.ts index a6639f1006..1b0e775e8c 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedInput.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedInput.ts @@ -1,11 +1,11 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import 'vs/css!./gettingStarted'; import { localize } from 'vs/nls'; -import { EditorInput } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; @@ -14,13 +14,14 @@ export const gettingStartedInputTypeId = 'workbench.editors.gettingStartedInput' export class GettingStartedInput extends EditorInput { static readonly ID = gettingStartedInputTypeId; + static readonly RESOURCE = URI.from({ scheme: Schemas.walkThrough, authority: 'vscode_getting_started_page' }); override get typeId(): string { return GettingStartedInput.ID; } get resource(): URI | undefined { - return URI.from({ scheme: Schemas.walkThrough, authority: 'vscode_getting_started_page' }); + return GettingStartedInput.RESOURCE; } override matches(other: unknown) { diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService.ts b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService.ts index 003abe99ab..c5fb8c3cd2 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService.ts @@ -3,13 +3,13 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createDecorator, IInstantiationService, optional, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { createDecorator, optional, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { Emitter, Event } from 'vs/base/common/event'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { Memento } from 'vs/workbench/common/memento'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, ContextKeyExpression, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Disposable } from 'vs/base/common/lifecycle'; import { IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { IExtensionDescription, IStartEntry } from 'vs/platform/extensions/common/extensions'; @@ -23,15 +23,18 @@ import { BuiltinGettingStartedCategory, BuiltinGettingStartedStep, BuiltinGettin import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; import { assertIsDefined } from 'vs/base/common/types'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { GettingStartedInput } from 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedInput'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { GettingStartedPage } from 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { LinkedText, parseLinkedText } from 'vs/base/common/linkedText'; +import { ILink, LinkedText, parseLinkedText } from 'vs/base/common/linkedText'; import { walkthroughsExtensionPoint } from 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedExtensionPoint'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { dirname } from 'vs/base/common/path'; +import { coalesce, flatten } from 'vs/base/common/arrays'; +import { IViewsService } from 'vs/workbench/common/views'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { isLinux, isMacintosh, isWindows, OperatingSystem as OS } from 'vs/base/common/platform'; +import { localize } from 'vs/nls'; + +export const WorkspacePlatform = new RawContextKey<'mac' | 'linux' | 'windows' | undefined>('workspacePlatform', undefined, localize('workspacePlatform', "The platform of the current workspace, which in remote contexts may be different from the platform of the UI")); export const IGettingStartedService = createDecorator('gettingStartedService'); @@ -52,10 +55,12 @@ export interface IGettingStartedStep { category: GettingStartedCategory | string when: ContextKeyExpression order: number - doneOn: { commandExecuted: string, eventFired?: never } | { eventFired: string, commandExecuted?: never } + /** @deprecated */ + doneOn?: { commandExecuted: string, eventFired?: never } | { eventFired: string, commandExecuted?: never } + completionEvents: string[] media: | { type: 'image', path: { hc: URI, light: URI, dark: URI }, altText: string } - | { type: 'markdown', path: URI, base: URI, } + | { type: 'markdown', path: URI, base: URI, root: URI } } export interface IGettingStartedWalkthroughDescriptor { @@ -63,6 +68,7 @@ export interface IGettingStartedWalkthroughDescriptor { title: string description: string order: number + next?: string icon: | { type: 'icon', icon: ThemeIcon } | { type: 'image', path: string } @@ -89,6 +95,7 @@ export interface IGettingStartedCategory { title: string description: string order: number + next?: string icon: | { type: 'icon', icon: ThemeIcon } | { type: 'image', path: string } @@ -116,8 +123,9 @@ export interface IGettingStartedCategoryWithProgress extends Omit - readonly onDidRemoveCategory: Event + readonly onDidAddCategory: Event + readonly onDidRemoveCategory: Event + readonly onDidChangeStep: Event readonly onDidChangeCategory: Event @@ -125,19 +133,23 @@ export interface IGettingStartedService { getCategories(): IGettingStartedCategoryWithProgress[] + registerWalkthrough(categoryDescriptor: IGettingStartedWalkthroughDescriptor, steps: IGettingStartedStep[]): void; + progressByEvent(eventName: string): void; progressStep(id: string): void; deprogressStep(id: string): void; + + installedExtensionsRegistered: Promise; } export class GettingStartedService extends Disposable implements IGettingStartedService { declare readonly _serviceBrand: undefined; - private readonly _onDidAddCategory = new Emitter(); - onDidAddCategory: Event = this._onDidAddCategory.event; + private readonly _onDidAddCategory = new Emitter(); + onDidAddCategory: Event = this._onDidAddCategory.event; - private readonly _onDidRemoveCategory = new Emitter(); - onDidRemoveCategory: Event = this._onDidRemoveCategory.event; + private readonly _onDidRemoveCategory = new Emitter(); + onDidRemoveCategory: Event = this._onDidRemoveCategory.event; private readonly _onDidChangeCategory = new Emitter(); onDidChangeCategory: Event = this._onDidChangeCategory.event; @@ -151,8 +163,8 @@ export class GettingStartedService extends Disposable implements IGettingStarted private memento: Memento; private stepProgress: Record; - private commandListeners = new Map(); - private eventListeners = new Map(); + private sessionEvents = new Set(); + private completionListeners = new Map>(); private gettingStartedContributions = new Map(); private steps = new Map(); @@ -160,18 +172,24 @@ export class GettingStartedService extends Disposable implements IGettingStarted private tasExperimentService?: ITASExperimentService; private sessionInstalledExtensions = new Set(); + private categoryVisibilityContextKeys = new Set(); + private stepCompletionContextKeyExpressions = new Set(); + private stepCompletionContextKeys = new Set(); + + private triggerInstalledExtensionsRegistered!: () => void; + installedExtensionsRegistered: Promise; + constructor( @IStorageService private readonly storageService: IStorageService, @ICommandService private readonly commandService: ICommandService, @IContextKeyService private readonly contextService: IContextKeyService, @IUserDataAutoSyncEnablementService readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService, @IProductService private readonly productService: IProductService, - @IEditorService private readonly editorService: IEditorService, - @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IInstantiationService private readonly instantiationService: IInstantiationService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IHostService private readonly hostService: IHostService, + @IViewsService private readonly viewsService: IViewsService, + @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, @optional(ITASExperimentService) tasExperimentService: ITASExperimentService, ) { super(); @@ -186,19 +204,67 @@ export class GettingStartedService extends Disposable implements IGettingStarted removed.forEach(e => this.unregisterExtensionContributions(e.description)); }); - this._register(this.commandService.onDidExecuteCommand(command => this.progressByCommand(command.commandId))); + this._register(this.commandService.onDidExecuteCommand(command => this.progressByEvent(`onCommand:${command.commandId}`))); + + this.extensionManagementService.getInstalled().then(installed => { + installed.forEach(ext => this.progressByEvent(`extensionInstalled:${ext.identifier.id.toLowerCase()}`)); + }); this._register(this.extensionManagementService.onDidInstallExtension(async e => { if (await this.hostService.hadLastFocus()) { - this.sessionInstalledExtensions.add(e.identifier.id); + this.sessionInstalledExtensions.add(e.identifier.id.toLowerCase()); + } + this.progressByEvent(`extensionInstalled:${e.identifier.id.toLowerCase()}`); + })); + + this._register(this.contextService.onDidChangeContext(event => { + if (event.affectsSome(this.categoryVisibilityContextKeys)) { this._onDidAddCategory.fire(); } + if (event.affectsSome(this.stepCompletionContextKeys)) { + this.stepCompletionContextKeyExpressions.forEach(expression => { + if (event.affectsSome(new Set(expression.keys())) && this.contextService.contextMatchesRules(expression)) { + this.progressByEvent(`onContext:` + expression.serialize()); + } + }); } })); - if (userDataAutoSyncEnablementService.isEnabled()) { this.progressByEvent('sync-enabled'); } - this._register(userDataAutoSyncEnablementService.onDidChangeEnablement(() => { - if (userDataAutoSyncEnablementService.isEnabled()) { this.progressByEvent('sync-enabled'); } + this._register(this.viewsService.onDidChangeViewVisibility(e => { + if (e.visible) { this.progressByEvent('onView:' + e.id); } })); + this._register(this.configurationService.onDidChangeConfiguration(e => { + e.affectedKeys.forEach(key => { this.progressByEvent('onSettingChanged:' + key); }); + })); + + this.remoteAgentService.getEnvironment().then(env => { + const remoteOS = env?.os; + + const remotePlatform = + remoteOS === OS.Macintosh ? 'mac' + : remoteOS === OS.Windows ? 'windows' + : remoteOS === OS.Linux ? 'linux' + : undefined; + + if (remotePlatform) { + WorkspacePlatform.bindTo(this.contextService).set(remotePlatform); + } else if (isMacintosh) { + WorkspacePlatform.bindTo(this.contextService).set('mac'); + } else if (isLinux) { + WorkspacePlatform.bindTo(this.contextService).set('linux'); + } else if (isWindows) { + WorkspacePlatform.bindTo(this.contextService).set('windows'); + } else { + WorkspacePlatform.bindTo(this.contextService).set(undefined); + } + }); + + if (userDataAutoSyncEnablementService.isEnabled()) { this.progressByEvent('onEvent:sync-enabled'); } + this._register(userDataAutoSyncEnablementService.onDidChangeEnablement(() => { + if (userDataAutoSyncEnablementService.isEnabled()) { this.progressByEvent('onEvent:sync-enabled'); } + })); + + this.installedExtensionsRegistered = new Promise(r => this.triggerInstalledExtensionsRegistered = r); + startEntries.forEach(async (entry, index) => { this.getCategoryOverrides(entry); this.registerStartEntry({ @@ -221,13 +287,23 @@ export class GettingStartedService extends Disposable implements IGettingStarted this.getStepOverrides(step, category.id); return ({ ...step, + completionEvents: step.completionEvents ?? [], description: parseDescription(step.description), category: category.id, order: index, when: ContextKeyExpr.deserialize(step.when) ?? ContextKeyExpr.true(), media: step.media.type === 'image' - ? { type: 'image', altText: step.media.altText, path: convertInternalMediaPathsToBrowserURIs(step.media.path) } - : { type: 'markdown', path: convertInternalMediaPathToFileURI(step.media.path), base: FileAccess.asFileUri('vs/workbench/contrib/welcome/gettingStarted/common/media/', require) }, + ? { + type: 'image', + altText: step.media.altText, + path: convertInternalMediaPathsToBrowserURIs(step.media.path) + } + : { + type: 'markdown', + path: convertInternalMediaPathToFileURI(step.media.path).with({ query: JSON.stringify({ moduleId: 'vs/workbench/contrib/welcome/gettingStarted/common/media/' + step.media.path }) }), + base: FileAccess.asFileUri('vs/workbench/contrib/welcome/gettingStarted/common/media/', require), + root: FileAccess.asFileUri('vs/workbench/contrib/welcome/gettingStarted/common/media/', require), + }, }); })); }); @@ -270,7 +346,7 @@ export class GettingStartedService extends Disposable implements IGettingStarted this._onDidChangeStep.fire(this.getStepProgress(existingStep)); } - private registerExtensionContributions(extension: IExtensionDescription) { + private async registerExtensionContributions(extension: IExtensionDescription) { const convertExtensionPathToFileURI = (path: string) => path.startsWith('https://') ? URI.parse(path, true) : FileAccess.asFileUri(joinPath(extension.extensionLocation, path)); @@ -292,55 +368,56 @@ export class GettingStartedService extends Disposable implements IGettingStarted } }; - let sectionToOpen: string | undefined; - if (!(extension.contributes?.walkthroughs?.length)) { return; } - if (this.productService.quality === 'stable') { - console.warn('Extension', extension.identifier.value, 'contributes welcome page content but this is a Stable build and extension contributions are only available in Insiders. The contributed content will be disregarded.'); - return; - } - - if (!this.configurationService.getValue('workbench.welcomePage.experimental.extensionContributions')) { - console.warn('Extension', extension.identifier.value, 'contributes welcome page content but the welcome page extension contribution feature flag has not been set. Set `workbench.welcomePage.experimental.extensionContributions` to begin using this experimental feature.'); - return; - } - - extension.contributes.startEntries?.forEach(entry => { - const entryID = extension.identifier.value + '#startEntry#' + idForStartEntry(entry); - this.registerStartEntry({ - content: { - type: 'startEntry', - command: entry.command, - }, - description: entry.description, - title: entry.title, - id: entryID, - order: 0, - when: ContextKeyExpr.deserialize(entry.when) ?? ContextKeyExpr.true(), - icon: { - type: 'image', - path: extension.icon - ? FileAccess.asBrowserUri(joinPath(extension.extensionLocation, extension.icon)).toString(true) - : DefaultIconPath - } + if (this.configurationService.getValue('workbench.welcomePage.experimental.startEntryContributions') && this.productService.quality !== 'stable') { + extension.contributes.startEntries?.forEach(entry => { + const entryID = extension.identifier.value + '#startEntry#' + idForStartEntry(entry); + this.registerStartEntry({ + content: { + type: 'startEntry', + command: entry.command, + }, + description: entry.description, + title: entry.title, + id: entryID, + order: 0, + when: ContextKeyExpr.deserialize(entry.when) ?? ContextKeyExpr.true(), + icon: { + type: 'image', + path: extension.icon + ? FileAccess.asBrowserUri(joinPath(extension.extensionLocation, extension.icon)).toString(true) + : DefaultIconPath + } + }); }); - }); + } - extension.contributes?.walkthroughs?.forEach(walkthrough => { - const categoryID = extension.identifier.value + '#walkthrough#' + walkthrough.id; + let sectionToOpen: string | undefined; + let sectionToOpenIndex = Math.min(); // '+Infinity'; + await Promise.all(extension.contributes?.walkthroughs?.map(async (walkthrough, index) => { + const categoryID = extension.identifier.value + '#' + walkthrough.id; + + const override = await Promise.race([ + this.tasExperimentService?.getTreatment(`gettingStarted.overrideCategory.${categoryID}.when`), + new Promise(resolve => setTimeout(() => resolve(walkthrough.when), 5000)) + ]); + if ( - this.sessionInstalledExtensions.has(extension.identifier.value) - && walkthrough.primary - && this.contextService.contextMatchesRules(ContextKeyExpr.deserialize(walkthrough.when) ?? ContextKeyExpr.true()) + this.sessionInstalledExtensions.has(extension.identifier.value.toLowerCase()) + && this.contextService.contextMatchesRules(ContextKeyExpr.deserialize(override ?? walkthrough.when) ?? ContextKeyExpr.true()) ) { - this.sessionInstalledExtensions.delete(extension.identifier.value); - sectionToOpen = categoryID; + this.sessionInstalledExtensions.delete(extension.identifier.value.toLowerCase()); + if (index < sectionToOpenIndex) { + sectionToOpen = categoryID; + sectionToOpenIndex = index; + } } - this.registerWalkthrough({ + + const walkthoughDescriptior = { content: { type: 'steps' }, description: walkthrough.description, title: walkthrough.title, @@ -352,60 +429,73 @@ export class GettingStartedService extends Disposable implements IGettingStarted ? FileAccess.asBrowserUri(joinPath(extension.extensionLocation, extension.icon)).toString(true) : DefaultIconPath }, - when: ContextKeyExpr.deserialize(walkthrough.when) ?? ContextKeyExpr.true(), - }, - (walkthrough.steps ?? (walkthrough as any).tasks).map((step, index) => { - const description = parseDescription(step.description); - const buttonDescription = (step as any as { button: LegacyButtonConfig }).button; - if (buttonDescription) { - description.push({ nodes: [{ href: buttonDescription.link ?? `command:${buttonDescription.command}`, label: buttonDescription.title }] }); - } - const fullyQualifiedID = extension.identifier.value + '#' + walkthrough.id + '#' + step.id; + when: ContextKeyExpr.deserialize(override ?? walkthrough.when) ?? ContextKeyExpr.true(), + } as const; - let media: IGettingStartedStep['media']; - if (typeof step.media.path === 'string' && step.media.path.endsWith('.md')) { + const steps = (walkthrough.steps ?? (walkthrough as any).tasks).map((step, index) => { + const description = parseDescription(step.description || ''); + const buttonDescription = (step as any as { button: LegacyButtonConfig }).button; + if (buttonDescription) { + description.push({ nodes: [{ href: buttonDescription.link ?? `command:${buttonDescription.command}`, label: buttonDescription.title }] }); + } + const fullyQualifiedID = extension.identifier.value + '#' + walkthrough.id + '#' + step.id; + + let media: IGettingStartedStep['media']; + + if (step.media.image) { + const altText = (step.media as any).altText; + if (altText === undefined) { + console.error('Getting Started: item', fullyQualifiedID, 'is missing altText for its media element.'); + } + media = { type: 'image', altText, path: convertExtensionRelativePathsToBrowserURIs(step.media.image) }; + } + else if (step.media.markdown) { + media = { + type: 'markdown', + path: convertExtensionPathToFileURI(step.media.markdown), + base: convertExtensionPathToFileURI(dirname(step.media.markdown)), + root: FileAccess.asFileUri(extension.extensionLocation), + }; + } + + // Legacy media config + else { + const legacyMedia = step.media as unknown as { path: string, altText: string }; + if (typeof legacyMedia.path === 'string' && legacyMedia.path.endsWith('.md')) { media = { type: 'markdown', - path: convertExtensionPathToFileURI(step.media.path), - base: convertExtensionPathToFileURI(dirname(step.media.path)) + path: convertExtensionPathToFileURI(legacyMedia.path), + base: convertExtensionPathToFileURI(dirname(legacyMedia.path)), + root: FileAccess.asFileUri(extension.extensionLocation), }; - } else { - const altText = (step.media as any).altText; - if (!altText) { + } + else { + const altText = legacyMedia.altText; + if (altText === undefined) { console.error('Getting Started: item', fullyQualifiedID, 'is missing altText for its media element.'); } - media = { type: 'image', altText, path: convertExtensionRelativePathsToBrowserURIs(step.media.path) }; + media = { type: 'image', altText, path: convertExtensionRelativePathsToBrowserURIs(legacyMedia.path) }; } - - return ({ - description, media, - doneOn: step.doneOn?.command - ? { commandExecuted: step.doneOn.command } - : { eventFired: 'markDone:' + fullyQualifiedID }, - id: fullyQualifiedID, - title: step.title, - when: ContextKeyExpr.deserialize(step.when) ?? ContextKeyExpr.true(), - category: categoryID, - order: index, - }); - })); - }); - - if (sectionToOpen) { - for (const group of this.editorGroupsService.groups) { - if (group.activeEditor instanceof GettingStartedInput) { - (group.activeEditorPane as GettingStartedPage).makeCategoryVisibleWhenAvailable(sectionToOpen); - return; } - } - if (this.configurationService.getValue('workbench.welcomePage.experimental.extensionContributions') === 'openToSide') { - this.editorService.openEditor(this.instantiationService.createInstance(GettingStartedInput, { selectedCategory: sectionToOpen }), {}, SIDE_GROUP); - } else if (this.configurationService.getValue('workbench.welcomePage.experimental.extensionContributions') === 'open') { - this.editorService.openEditor(this.instantiationService.createInstance(GettingStartedInput, { selectedCategory: sectionToOpen }), {}); - } else if (this.configurationService.getValue('workbench.welcomePage.experimental.extensionContributions') === 'openInBackground') { - this.editorService.openEditor(this.instantiationService.createInstance(GettingStartedInput, { selectedCategory: sectionToOpen }), { inactive: true }); - } + return ({ + description, media, + completionEvents: step.completionEvents?.filter(x => typeof x === 'string') ?? [], + id: fullyQualifiedID, + title: step.title, + when: ContextKeyExpr.deserialize(step.when) ?? ContextKeyExpr.true(), + category: categoryID, + order: index, + }); + }); + + this.registerWalkthrough(walkthoughDescriptior, steps); + })); + + this.triggerInstalledExtensionsRegistered(); + + if (sectionToOpen && this.configurationService.getValue('workbench.welcomePage.walkthroughs.openOnInstall')) { + this.commandService.executeCommand('workbench.action.openWalkthrough', sectionToOpen); } } @@ -417,7 +507,7 @@ export class GettingStartedService extends Disposable implements IGettingStarted extension.contributes?.startEntries?.forEach(section => { const categoryID = extension.identifier.value + '#startEntry#' + idForStartEntry(section); this.gettingStartedContributions.delete(categoryID); - this._onDidRemoveCategory.fire(categoryID); + this._onDidRemoveCategory.fire(); }); extension.contributes?.walkthroughs?.forEach(section => { @@ -427,25 +517,86 @@ export class GettingStartedService extends Disposable implements IGettingStarted this.steps.delete(fullyQualifiedID); }); this.gettingStartedContributions.delete(categoryID); - this._onDidRemoveCategory.fire(categoryID); + this._onDidRemoveCategory.fire(); }); } private registerDoneListeners(step: IGettingStartedStep) { - if (step.doneOn.commandExecuted) { - const existing = this.commandListeners.get(step.doneOn.commandExecuted); - if (existing) { existing.push(step.id); } - else { - this.commandListeners.set(step.doneOn.commandExecuted, [step.id]); + if (step.doneOn) { + if (step.doneOn.commandExecuted) { step.completionEvents.push(`onCommand:${step.doneOn.commandExecuted}`); } + if (step.doneOn.eventFired) { step.completionEvents.push(`onEvent:${step.doneOn.eventFired}`); } + } + + if (!step.completionEvents.length) { + step.completionEvents = coalesce(flatten( + step.description + .filter(linkedText => linkedText.nodes.length === 1) // only buttons + .map(linkedText => + linkedText.nodes + .filter(((node): node is ILink => typeof node !== 'string')) + .map(({ href }) => { + if (href.startsWith('command:')) { + return 'onCommand:' + href.slice('command:'.length, href.includes('?') ? href.indexOf('?') : undefined); + } + if (href.startsWith('https://') || href.startsWith('http://')) { + return 'onLink:' + href; + } + return undefined; + })))); + } + + if (!step.completionEvents.length) { + step.completionEvents.push('stepSelected'); + } + + for (let event of step.completionEvents) { + const [_, eventType, argument] = /^([^:]*):?(.*)$/.exec(event) ?? []; + + if (!eventType) { + console.error(`Unknown completionEvent ${event} when registering step ${step.id}`); + continue; + } + + switch (eventType) { + case 'onLink': case 'onEvent': case 'onView': case 'onSettingChanged': + break; + case 'onContext': { + const expression = ContextKeyExpr.deserialize(argument); + if (expression) { + this.stepCompletionContextKeyExpressions.add(expression); + expression.keys().forEach(key => this.stepCompletionContextKeys.add(key)); + event = eventType + ':' + expression.serialize(); + } else { + console.error('Unable to parse context key expression:', expression, 'in getting started step', step.id); + } + break; + } + case 'stepSelected': + event = eventType + ':' + step.id; + break; + case 'onCommand': + event = eventType + ':' + argument.replace(/^toSide:/, ''); + break; + case 'extensionInstalled': + event = eventType + ':' + argument.toLowerCase(); + break; + default: + console.error(`Unknown completionEvent ${event} when registering step ${step.id}`); + continue; + } + + this.registerCompletionListener(event, step); + if (this.sessionEvents.has(event)) { + this.progressStep(step.id); } } - if (step.doneOn.eventFired) { - const existing = this.eventListeners.get(step.doneOn.eventFired); - if (existing) { existing.push(step.id); } - else { - this.eventListeners.set(step.doneOn.eventFired, [step.id]); - } + } + + private registerCompletionListener(event: string, step: IGettingStartedStep) { + if (!this.completionListeners.has(event)) { + this.completionListeners.set(event, new Set()); } + this.completionListeners.get(event)?.add(step.id); } getCategories(): IGettingStartedCategoryWithProgress[] { @@ -515,14 +666,11 @@ export class GettingStartedService extends Disposable implements IGettingStarted this._onDidProgressStep.fire(this.getStepProgress(step)); } - private progressByCommand(command: string) { - const listening = this.commandListeners.get(command) ?? []; - listening.forEach(id => this.progressStep(id)); - } - progressByEvent(event: string): void { - const listening = this.eventListeners.get(event) ?? []; - listening.forEach(id => this.progressStep(id)); + if (this.sessionEvents.has(event)) { return; } + + this.sessionEvents.add(event); + this.completionListeners.get(event)?.forEach(id => this.progressStep(id)); } private registerStartEntry(categoryDescriptor: IGettingStartedStartEntryDescriptor): void { @@ -535,10 +683,10 @@ export class GettingStartedService extends Disposable implements IGettingStarted const category: IGettingStartedCategory = { ...categoryDescriptor }; this.gettingStartedContributions.set(categoryDescriptor.id, category); - this._onDidAddCategory.fire(this.getCategoryProgress(category)); + this._onDidAddCategory.fire(); } - private registerWalkthrough(categoryDescriptor: IGettingStartedWalkthroughDescriptor, steps: IGettingStartedStep[]): void { + registerWalkthrough(categoryDescriptor: IGettingStartedWalkthroughDescriptor, steps: IGettingStartedStep[]): void { const oldCategory = this.gettingStartedContributions.get(categoryDescriptor.id); if (oldCategory) { console.error(`Skipping attempt to overwrite getting started category. (${categoryDescriptor.id})`); @@ -551,8 +699,28 @@ export class GettingStartedService extends Disposable implements IGettingStarted if (this.steps.has(step.id)) { throw Error('Attempting to register step with id ' + step.id + ' twice. Second is dropped.'); } this.steps.set(step.id, step); this.registerDoneListeners(step); + step.when.keys().forEach(key => this.categoryVisibilityContextKeys.add(key)); }); - this._onDidAddCategory.fire(this.getCategoryProgress(category)); + + if (this.contextService.contextMatchesRules(category.when)) { + this._onDidAddCategory.fire(); + } + + this.tasExperimentService?.getTreatment(`gettingStarted.overrideCategory.${categoryDescriptor.id.replace('#', '.')}.when`).then(override => { + if (override) { + const old = category.when; + const gnu = ContextKeyExpr.deserialize(override) ?? old; + this.categoryVisibilityContextKeys.add(override); + category.when = gnu; + + if (this.contextService.contextMatchesRules(old) && !this.contextService.contextMatchesRules(gnu)) { + this._onDidRemoveCategory.fire(); + } else if (!this.contextService.contextMatchesRules(old) && this.contextService.contextMatchesRules(gnu)) { + this._onDidAddCategory.fire(); + } + } + }); + category.when.keys().forEach(key => this.categoryVisibilityContextKeys.add(key)); } private getStep(id: string): IGettingStartedStep { diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/common/gettingStartedContent.ts b/src/vs/workbench/contrib/welcome/gettingStarted/common/gettingStartedContent.ts index 87a21287d6..b11e31b16a 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/common/gettingStartedContent.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/common/gettingStartedContent.ts @@ -3,6 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/workbench/contrib/welcome/gettingStarted/common/media/example_markdown_media'; +import 'vs/workbench/contrib/welcome/gettingStarted/common/media/notebookProfile'; import { localize } from 'vs/nls'; import { Codicon } from 'vs/base/common/codicons'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; @@ -12,14 +14,13 @@ import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; const setupIcon = registerIcon('getting-started-setup', Codicon.zap, localize('getting-started-setup-icon', "Icon used for the setup category of getting started")); const beginnerIcon = registerIcon('getting-started-beginner', Codicon.lightbulb, localize('getting-started-beginner-icon', "Icon used for the beginner category of getting started")); const intermediateIcon = registerIcon('getting-started-intermediate', Codicon.mortarBoard, localize('getting-started-intermediate-icon', "Icon used for the intermediate category of getting started")); -const codespacesIcon = registerIcon('getting-started-codespaces', Codicon.github, localize('getting-started-codespaces-icon', "Icon used for the codespaces category of getting started")); export type BuiltinGettingStartedStep = { id: string title: string, description: string, - doneOn: { commandExecuted: string, eventFired?: never } | { eventFired: string, commandExecuted?: never, } + completionEvents?: string[] when?: string, media: | { type: 'image', path: string | { hc: string, light: string, dark: string }, altText: string } @@ -30,6 +31,7 @@ export type BuiltinGettingStartedCategory = { id: string title: string, description: string, + next?: string, icon: ThemeIcon, when?: string, content: @@ -116,83 +118,32 @@ export const startEntries: GettingStartedStartEntryContent = [ }, ]; +const Button = (title: string, href: string) => `[${title}](${href})`; + export const walkthroughs: GettingStartedWalkthroughContent = [ - { - id: 'Codespaces', - title: localize('gettingStarted.codespaces.title', "Primer on Codespaces"), - icon: codespacesIcon, - when: 'remoteName == codespaces', - description: localize('gettingStarted.codespaces.description', "Get up and running with your instant code environment."), - content: { - type: 'steps', - steps: [ - { - id: 'runProjectStep', - title: localize('gettingStarted.runProject.title', "Build & run your app"), - description: localize('gettingStarted.runProject.description', "Build, run & debug your code in the cloud, right from the browser.\n[Start Debugging](command:workbench.action.debug.selectandstart)"), - doneOn: { commandExecuted: 'workbench.action.debug.selectandstart' }, - media: { type: 'image', altText: 'Node.js project running debug mode and paused.', path: 'runProject.png' }, - }, - { - id: 'forwardPortsStep', - title: localize('gettingStarted.forwardPorts.title', "Access your running application"), - description: localize('gettingStarted.forwardPorts.description', "Ports running within your codespace are automatically forwarded to the web, so you can open them in your browser.\n[Show Ports Panel](command:~remote.forwardedPorts.focus)"), - doneOn: { commandExecuted: '~remote.forwardedPorts.focus' }, - media: { type: 'image', altText: 'Ports panel.', path: 'forwardPorts.png' }, - }, - { - id: 'pullRequests', - title: localize('gettingStarted.pullRequests.title', "Pull requests at your fingertips"), - description: localize('gettingStarted.pullRequests.description', "Bring your GitHub workflow closer to your code, so you can review pull requests, add comments, merge branches, and more.\n[Open GitHub View](command:workbench.view.extension.github-pull-requests)"), - doneOn: { commandExecuted: 'workbench.view.extension.github-pull-requests' }, - media: { type: 'image', altText: 'Preview for reviewing a pull request.', path: 'pullRequests.png' }, - }, - { - id: 'remoteTerminal', - title: localize('gettingStarted.remoteTerminal.title', "Run tasks in the integrated terminal"), - description: localize('gettingStarted.remoteTerminal.description', "Perform quick command-line tasks using the built-in terminal.\n[Focus Terminal](command:terminal.focus)"), - doneOn: { commandExecuted: 'terminal.focus' }, - media: { type: 'image', altText: 'Remote terminal showing npm commands.', path: 'remoteTerminal.png' }, - }, - { - id: 'openVSC', - title: localize('gettingStarted.openVSC.title', "Develop remotely in VS Code"), - description: localize('gettingStarted.openVSC.description', "Access the power of your cloud development environment from your local VS Code. Set it up by installing the GitHub Codespaces extension and connecting your GitHub account.\n[Open in VS Code](command:github.codespaces.openInStable)"), - when: 'isWeb', - doneOn: { commandExecuted: 'github.codespaces.openInStable' }, - media: { - type: 'image', altText: 'Preview of the Open in VS Code command.', path: { - dark: 'dark/openVSC.png', - light: 'light/openVSC.png', - hc: 'light/openVSC.png', - } - }, - } - ] - } - }, - { id: 'Setup', - title: localize('gettingStarted.setup.title', "Customize your Setup"), - description: localize('gettingStarted.setup.description', "Extend and customize VS Code to make it yours."), + title: localize('gettingStarted.setup.title', "Get Started with VS Code"), + description: localize('gettingStarted.setup.description', "Discover the best customizations to make VS Code yours."), icon: setupIcon, - when: 'remoteName != codespaces', + next: 'Beginner', content: { type: 'steps', steps: [ { id: 'pickColorTheme', - title: localize('gettingStarted.pickColor.title', "Customize the look with themes"), - description: localize('gettingStarted.pickColor.description', "Pick a color theme to match your taste and mood while coding.\n[Pick a Theme](command:workbench.action.selectTheme)"), - doneOn: { commandExecuted: 'workbench.action.selectTheme' }, - media: { type: 'image', altText: 'Color theme preview for dark and light theme.', path: 'colorTheme.png', } + title: localize('gettingStarted.pickColor.title', "Choose the look you want"), + description: localize('gettingStarted.pickColor.description.interpolated', "The right color palette helps you focus on your code, is easy on your eyes, and is simply more fun to use.\n{0}", Button(localize('titleID', "Browse Color Themes"), 'command:workbench.action.selectTheme')), + completionEvents: [ + 'onSettingChanged:workbench.colorTheme', + 'onCommand:workbench.action.selectTheme' + ], + media: { type: 'markdown', path: 'example_markdown_media', } }, { id: 'findLanguageExtensions', - title: localize('gettingStarted.findLanguageExts.title', "Code in any language"), - description: localize('gettingStarted.findLanguageExts.description', "VS Code supports over 50+ programming languages. While many are built-in, others can be easily installed as extensions in one click.\n[Browse Language Extensions](command:workbench.extensions.action.showLanguageExtensions)"), - doneOn: { commandExecuted: 'workbench.extensions.action.showLanguageExtensions' }, + title: localize('gettingStarted.findLanguageExts.title', "Rich support for all your languages"), + description: localize('gettingStarted.findLanguageExts.description.interpolated', "Code smarter with syntax highlighting, code completion, linting and debugging. While many languages are built-in, many more can be added as extensions.\n{0}", Button(localize('browseLangExts', "Browse Language Extensions"), 'command:workbench.extensions.action.showLanguageExtensions')), media: { type: 'image', altText: 'Language extensions', path: { dark: 'dark/languageExtensions.png', @@ -202,38 +153,35 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ } }, { - id: 'keymaps', - title: localize('gettingStarted.keymaps.title', "Switch from other editors"), - description: localize('gettingStarted.keymaps.description', "Bring your favorite keyboard shortcuts from other editors into VS Code with keymaps.\n[Browse Keymap Extensions](command:workbench.extensions.action.showRecommendedKeymapExtensions)"), - doneOn: { commandExecuted: 'workbench.extensions.action.showRecommendedKeymapExtensions' }, + id: 'commandPaletteTask', + title: localize('gettingStarted.commandPalette.title', "One shortcut to access everything"), + description: localize('gettingStarted.commandPalette.description.interpolated', "Commands Palette is the keyboard way to accomplish any task in VS Code. **Practice** by looking up your frequently used commands to save time and keep in the flow.\n{0}\n__Try searching for 'view toggle'.__", Button(localize('commandPalette', "Open Command Palette"), 'command:workbench.action.showCommands')), media: { - type: 'image', altText: 'List of keymap extensions.', path: { - dark: 'dark/keymaps.png', - light: 'light/keymaps.png', - hc: 'hc/keymaps.png', - }, - } + type: 'image', altText: 'Command Palette overlay for searching and executing commands.', path: { + dark: 'dark/commandPalette.png', + light: 'light/commandPalette.png', + hc: 'hc/commandPalette.png', + } + }, }, { - id: 'settingsSync', - title: localize('gettingStarted.settingsSync.title', "Sync your favorite setup"), - description: localize('gettingStarted.settingsSync.description', "Never lose the perfect VS Code setup! Settings Sync will back up and share settings, keybindings & extensions across several VS Code instances.\n[Enable Settings Sync](command:workbench.userDataSync.actions.turnOn)"), - when: 'syncStatus != uninitialized', - doneOn: { eventFired: 'sync-enabled' }, + id: 'workspaceTrust', + title: localize('gettingStarted.workspaceTrust.title', "Safely browse and edit code"), + description: localize('gettingStarted.workspaceTrust.description.interpolated', "{0} lets you decide whether your project folders should **allow or restrict** automatic code execution __(required for extensions, debugging, etc)__.\nOpening a file/folder will prompt to grant trust. You can always {1} later.", Button(localize('workspaceTrust', "Workspace Trust"), 'https://github.com/microsoft/vscode-docs/blob/workspaceTrust/docs/editor/workspace-trust.md'), Button(localize('enableTrust', "enable trust"), 'command:toSide:workbench.action.manageTrustedDomain')), + when: '!isWorkspaceTrusted && workspaceFolderCount == 0', media: { - type: 'image', altText: 'The "Turn on Sync" entry in the settings gear menu.', path: { - dark: 'dark/settingsSync.png', - light: 'light/settingsSync.png', - hc: 'hc/settingsSync.png', + type: 'image', altText: 'Workspace Trust editor in Restricted mode and a primary button for switching to Trusted mode.', path: { + dark: 'dark/workspaceTrust.svg', + light: 'light/workspaceTrust.svg', + hc: 'dark/workspaceTrust.svg', }, - } + }, }, { id: 'pickAFolderTask-Mac', - title: localize('gettingStarted.setup.OpenFolder.title', "Open your project folder"), - description: localize('gettingStarted.setup.OpenFolder.description', "Open a project folder to start coding!\n[Pick a Folder](command:workbench.action.files.openFileFolder)"), + title: localize('gettingStarted.setup.OpenFolder.title', "Open up your code"), + description: localize('gettingStarted.setup.OpenFolder.description.interpolated', "You're all set to start coding. Open a project folder to get your files into VS Code.\n{0}", Button(localize('pickFolder', "Pick a Folder"), 'command:workbench.action.files.openFileFolder')), when: 'isMac && workspaceFolderCount == 0', - doneOn: { commandExecuted: 'workbench.action.files.openFileFolder' }, media: { type: 'image', altText: 'Explorer view showing buttons for opening folder and cloning repository.', path: { dark: 'dark/openFolder.png', @@ -244,10 +192,9 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ }, { id: 'pickAFolderTask-Other', - title: localize('gettingStarted.setup.OpenFolder.title', "Open your project folder"), - description: localize('gettingStarted.setup.OpenFolder.description2', "Open a project folder to start coding!\n[Pick a Folder](command:workbench.action.files.openFolder)"), + title: localize('gettingStarted.setup.OpenFolder.title', "Open up your code"), + description: localize('gettingStarted.setup.OpenFolder.description.interpolated', "You're all set to start coding. Open a project folder to get your files into VS Code.\n{0}", Button(localize('pickFolder', "Pick a Folder"), 'command:workbench.action.files.openFolder')), when: '!isMac && workspaceFolderCount == 0', - doneOn: { commandExecuted: 'workbench.action.files.openFolder' }, media: { type: 'image', altText: 'Explorer view showing buttons for opening folder and cloning repository.', path: { dark: 'dark/openFolder.png', @@ -258,10 +205,9 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ }, { id: 'quickOpen', - title: localize('gettingStarted.quickOpen.title', "Quick open files"), - description: localize('gettingStarted.quickOpen.description', "Navigate between files in an instant with one keystroke. Tip: Open multiple files by pressing the right arrow key.\n[Quick Open a File](command:toSide:workbench.action.quickOpen)"), + title: localize('gettingStarted.quickOpen.title', "Quickly navigate between your files"), + description: localize('gettingStarted.quickOpen.description.interpolated', "Navigate between files in an instant with one keystroke. Tip: Open multiple files by pressing the right arrow key.\n{0}", Button(localize('quickOpen', "Quick Open a File"), 'command:toSide:workbench.action.quickOpen')), when: 'workspaceFolderCount != 0', - doneOn: { commandExecuted: 'workbench.action.quickOpen' }, media: { type: 'image', altText: 'Go to file in quick search.', path: { dark: 'dark/openFolder.png', @@ -278,29 +224,28 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ id: 'Beginner', title: localize('gettingStarted.beginner.title', "Learn the Fundamentals"), icon: beginnerIcon, + next: 'Intermediate', description: localize('gettingStarted.beginner.description', "Jump right into VS Code and get an overview of the must-have features."), content: { type: 'steps', steps: [ { - id: 'commandPaletteTask', - title: localize('gettingStarted.commandPalette.title', "Find & run commands"), - description: localize('gettingStarted.commandPalette.description', "The easiest way to find everything VS Code can do. If you're ever looking for a feature or a shortcut, check here first!\n[Open Command Palette](command:workbench.action.showCommands)"), - doneOn: { commandExecuted: 'workbench.action.showCommands' }, + id: 'playground', + title: localize('gettingStarted.playground.title', "Redefine your editing skills"), + description: localize('gettingStarted.playground.description.interpolated', "Want to code faster and smarter? Practice powerful code editing features in the interactive playground.\n{0}", Button(localize('openInteractivePlayground', "Open Interactive Playground"), 'command:toSide:workbench.action.showInteractivePlayground')), media: { - type: 'image', altText: 'Command Palette overlay for searching and executing commands.', path: { - dark: 'dark/commandPalette.png', - light: 'light/commandPalette.png', - hc: 'hc/commandPalette.png', - } + type: 'image', altText: 'Interactive Playground.', path: { + dark: 'dark/playground.png', + light: 'light/playground.png', + hc: 'light/playground.png' + }, }, }, { id: 'terminal', title: localize('gettingStarted.terminal.title', "Convenient built-in terminal"), - description: localize('gettingStarted.terminal.description', "Quickly run shell commands and monitor build output, right next to your code.\n[Show Terminal Panel](command:workbench.action.terminal.toggleTerminal)"), + description: localize('gettingStarted.terminal.description.interpolated', "Quickly run shell commands and monitor build output, right next to your code.\n{0}", Button(localize('showTerminal', "Show Terminal Panel"), 'command:workbench.action.terminal.toggleTerminal')), when: 'remoteName != codespaces && !terminalIsOpen', - doneOn: { commandExecuted: 'workbench.action.terminal.toggleTerminal' }, media: { type: 'image', altText: 'Integrated terminal running a few npm commands', path: { dark: 'dark/terminal.png', @@ -312,8 +257,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ { id: 'extensions', title: localize('gettingStarted.extensions.title', "Limitless extensibility"), - description: localize('gettingStarted.extensions.description', "Extensions are VS Code's power-ups. They range from handy productivity hacks, expanding out-of-the-box features, to adding completely new capabilities.\n[Browse Recommended Extensions](command:workbench.extensions.action.showRecommendedExtensions)"), - doneOn: { commandExecuted: 'workbench.extensions.action.showRecommendedExtensions' }, + description: localize('gettingStarted.extensions.description.interpolated', "Extensions are VS Code's power-ups. They range from handy productivity hacks, expanding out-of-the-box features, to adding completely new capabilities.\n{0}", Button(localize('browseRecommended', "Browse Recommended Extensions"), 'command:workbench.extensions.action.showRecommendedExtensions')), media: { type: 'image', altText: 'VS Code extension marketplace with featured language extensions', path: { dark: 'dark/extensions.png', @@ -325,8 +269,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ { id: 'settings', title: localize('gettingStarted.settings.title', "Tune your settings"), - description: localize('gettingStarted.settings.description', "Tweak every aspect of VS Code and your extensions to your liking. Commonly used settings are listed first to get you started.\n[Tweak my Settings](command:toSide:workbench.action.openSettings)"), - doneOn: { commandExecuted: 'workbench.action.openSettings' }, + description: localize('gettingStarted.settings.description.interpolated', "Tweak every aspect of VS Code and your extensions to your liking. Commonly used settings are listed first to get you started.\n{0}", Button(localize('tweakSettings', "Tweak my Settings"), 'command:toSide:workbench.action.openSettings')), media: { type: 'image', altText: 'VS Code Settings', path: { dark: 'dark/settings.png', @@ -335,11 +278,24 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ } }, }, + { + id: 'settingsSync', + title: localize('gettingStarted.settingsSync.title', "Sync your stuff across devices"), + description: localize('gettingStarted.settingsSync.description.interpolated', "Never lose the perfect VS Code setup! Settings Sync will back up and share settings, keybindings & extensions across several installations.\n{0}", Button(localize('enableSync', "Enable Settings Sync"), 'command:workbench.userDataSync.actions.turnOn')), + when: 'syncStatus != uninitialized', + completionEvents: ['onEvent:sync-enabled'], + media: { + type: 'image', altText: 'The "Turn on Sync" entry in the settings gear menu.', path: { + dark: 'dark/settingsSync.png', + light: 'light/settingsSync.png', + hc: 'hc/settingsSync.png', + }, + } + }, { id: 'videoTutorial', title: localize('gettingStarted.videoTutorial.title', "Lean back and learn"), - description: localize('gettingStarted.videoTutorial.description', "Watch the first in a series of short & practical video tutorials for VS Code's key features.\n[Watch Tutorial](https://aka.ms/vscode-getting-started-video)"), - doneOn: { eventFired: 'linkOpened:https://aka.ms/vscode-getting-started-video' }, + description: localize('gettingStarted.videoTutorial.description.interpolated', "Watch the first in a series of short & practical video tutorials for VS Code's key features.\n{0}", Button(localize('watch', "Watch Tutorial"), 'https://aka.ms/vscode-getting-started-video')), media: { type: 'image', altText: 'VS Code Settings', path: 'tutorialVideo.png' }, } ] @@ -354,24 +310,10 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ content: { type: 'steps', steps: [ - { - id: 'playground', - title: localize('gettingStarted.playground.title', "Redefine your editing skills"), - description: localize('gettingStarted.playground.description', "Want to code faster and smarter? Practice powerful code editing features in the interactive playground.\n[Open Interactive Playground](command:toSide:workbench.action.showInteractivePlayground)"), - doneOn: { commandExecuted: 'workbench.action.showInteractivePlayground' }, - media: { - type: 'image', altText: 'Interactive Playground.', path: { - dark: 'dark/playground.png', - light: 'light/playground.png', - hc: 'light/playground.png' - }, - }, - }, { id: 'splitview', title: localize('gettingStarted.splitview.title', "Side by side editing"), - description: localize('gettingStarted.splitview.description', "Make the most of your screen estate by opening files side by side, vertically and horizontally.\n[Split Editor](command:workbench.action.splitEditor)"), - doneOn: { commandExecuted: 'workbench.action.splitEditor' }, + description: localize('gettingStarted.splitview.description.interpolated', "Make the most of your screen estate by opening files side by side, vertically and horizontally.\n{0}", Button(localize('splitEditor', "Split Editor"), 'command:workbench.action.splitEditor')), media: { type: 'image', altText: 'Multiple editors in split view.', path: { dark: 'dark/splitview.png', @@ -383,9 +325,8 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ { id: 'debugging', title: localize('gettingStarted.debug.title', "Watch your code in action"), - description: localize('gettingStarted.debug.description', "Accelerate your edit, build, test, and debug loop by setting up a launch configuration.\n[Run your Project](command:workbench.action.debug.selectandstart)"), + description: localize('gettingStarted.debug.description.interpolated', "Accelerate your edit, build, test, and debug loop by setting up a launch configuration.\n{0}", Button(localize('runProject', "Run your Project"), 'command:workbench.action.debug.selectandstart')), when: 'workspaceFolderCount != 0', - doneOn: { commandExecuted: 'workbench.action.debug.selectandstart' }, media: { type: 'image', altText: 'Run and debug view.', path: { dark: 'dark/debug.png', @@ -397,9 +338,8 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ { id: 'scmClone', title: localize('gettingStarted.scm.title', "Track your code with Git"), - description: localize('gettingStarted.scmClone.description', "Set up the built-in version control for your project to track your changes and collaborate with others.\n[Clone Repository](command:git.clone)"), + description: localize('gettingStarted.scmClone.description.interpolated', "Set up the built-in version control for your project to track your changes and collaborate with others.\n{0}", Button(localize('cloneRepo', "Clone Repository"), 'command:git.clone')), when: 'config.git.enabled && !git.missing && workspaceFolderCount == 0', - doneOn: { commandExecuted: 'git.clone' }, media: { type: 'image', altText: 'Source Control view.', path: { dark: 'dark/scm.png', @@ -411,9 +351,8 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ { id: 'scmSetup', title: localize('gettingStarted.scm.title', "Track your code with Git"), - description: localize('gettingStarted.scmSetup.description', "Set up the built-in version control for your project to track your changes and collaborate with others.\n[Initialize Git Repository](command:git.init)"), + description: localize('gettingStarted.scmSetup.description.interpolated', "Set up the built-in version control for your project to track your changes and collaborate with others.\n{0}", Button(localize('initRepo', "Initialize Git Repository"), 'command:git.init')), when: 'config.git.enabled && !git.missing && workspaceFolderCount != 0 && gitOpenRepositoryCount == 0', - doneOn: { commandExecuted: 'git.init' }, media: { type: 'image', altText: 'Source Control view.', path: { dark: 'dark/scm.png', @@ -425,9 +364,8 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ { id: 'scm', title: localize('gettingStarted.scm.title', "Track your code with Git"), - description: localize('gettingStarted.scm.description', "No more looking up Git commands! Git and GitHub workflows are seamlessly integrated.[Open Source Control](command:workbench.view.scm)"), + description: localize('gettingStarted.scm.description.interpolated', "No more looking up Git commands! Git and GitHub workflows are seamlessly integrated.\n{0}", Button(localize('openSCM', "Open Source Control"), 'command:workbench.view.scm')), when: 'config.git.enabled && !git.missing && workspaceFolderCount != 0 && gitOpenRepositoryCount != 0 && activeViewlet != \'workbench.view.scm\'', - doneOn: { commandExecuted: 'workbench.view.scm.focus' }, media: { type: 'image', altText: 'Source Control view.', path: { dark: 'dark/scm.png', @@ -440,8 +378,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ id: 'tasks', title: localize('gettingStarted.tasks.title', "Automate your project tasks"), when: 'workspaceFolderCount != 0', - description: localize('gettingStarted.tasks.description', "Create tasks for your common workflows and enjoy the integrated experience of running scripts and automatically checking results.\n[Run Auto-detected Tasks](command:workbench.action.tasks.runTask)"), - doneOn: { commandExecuted: 'workbench.action.tasks.runTask' }, + description: localize('gettingStarted.tasks.description.interpolated', "Create tasks for your common workflows and enjoy the integrated experience of running scripts and automatically checking results.\n{0}", Button(localize('runTasks', "Run Auto-detected Tasks"), 'command:workbench.action.tasks.runTask')), media: { type: 'image', altText: 'Task runner.', path: { dark: 'dark/tasks.png', @@ -453,8 +390,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ { id: 'shortcuts', title: localize('gettingStarted.shortcuts.title', "Customize your shortcuts"), - description: localize('gettingStarted.shortcuts.description', "Once you have discovered your favorite commands, create custom keyboard shortcuts for instant access.\n[Keyboard Shortcuts](command:toSide:workbench.action.openGlobalKeybindings)"), - doneOn: { commandExecuted: 'workbench.action.openGlobalKeybindings' }, + description: localize('gettingStarted.shortcuts.description.interpolated', "Once you have discovered your favorite commands, create custom keyboard shortcuts for instant access.\n{0}", Button(localize('keyboardShortcuts', "Keyboard Shortcuts"), 'command:toSide:workbench.action.openGlobalKeybindings')), media: { type: 'image', altText: 'Interactive shortcuts.', path: { dark: 'dark/shortcuts.png', @@ -465,5 +401,27 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ } ] } + }, + { + id: 'notebooks', + title: localize('gettingStarted.notebook.title', "Customize Notebooks"), + description: '', + icon: setupIcon, + when: 'config.notebook.experimental.gettingStarted && userHasOpenedNotebook', + content: { + type: 'steps', + steps: [ + { + completionEvents: ['onCommand:notebook.setProfile'], + id: 'notebookProfile', + title: localize('gettingStarted.notebookProfile.title', "Select the layout for your notebooks"), + description: localize('gettingStarted.notebookProfile.description', "Get notebooks to feel just the way you prefer"), + when: 'userHasOpenedNotebook', + media: { + type: 'markdown', path: 'notebookProfile' + } + }, + ] + } } ]; diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark.png b/src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark.png new file mode 100644 index 0000000000000000000000000000000000000000..bac2bc8d2d80329b01a1906e0266b503b46bc4f6 GIT binary patch literal 14165 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 Gq5lURohw@a literal 0 HcmV?d00001 diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark/keymaps.png b/src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark/keymaps.png deleted file mode 100644 index b4027937a3ab6262050ccf79c07ec239fe74105e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49149 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
hMTr=wC0kcwHE|zQ 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
&1VF-W*R?wa(aJfoh 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

SxT>H&a&6vs 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 diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark/openVSC.png b/src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark/openVSC.png deleted file mode 100644 index 9a700e0d4535b58c73281e8d6d9c868d5c59e862..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7282 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 diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark/workspaceTrust.svg b/src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark/workspaceTrust.svg new file mode 100644 index 0000000000..f1cf7fe505 --- /dev/null +++ b/src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark/workspaceTrust.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/common/media/example_markdown_media.ts b/src/vs/workbench/contrib/welcome/gettingStarted/common/media/example_markdown_media.ts new file mode 100644 index 0000000000..66b518f81f --- /dev/null +++ b/src/vs/workbench/contrib/welcome/gettingStarted/common/media/example_markdown_media.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { escape } from 'vs/base/common/strings'; +import { localize } from 'vs/nls'; + +export default () => ` + + + + + ${escape(localize('light', "Light"))} + + + + ${escape(localize('dark', "Dark"))} + + + + ${escape(localize('HighContrast', "High Contrast"))} + + + + ${escape(localize('seeMore', "See More Themes..."))} + + +`; diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/common/media/forwardPorts.png b/src/vs/workbench/contrib/welcome/gettingStarted/common/media/forwardPorts.png deleted file mode 100644 index ae10f31d6f6d10b33cfa1fd9a35d1518e850ee43..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17327 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 M07*qoM6N<$g4){C@&Et; diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/common/media/hc/keymaps.png b/src/vs/workbench/contrib/welcome/gettingStarted/common/media/hc/keymaps.png deleted file mode 100644 index 6105fd0f714736b48b54df920dc0c54247a909d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50010 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 N002ovPDHLkV1my_qC@}y diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/common/media/light.png b/src/vs/workbench/contrib/welcome/gettingStarted/common/media/light.png new file mode 100644 index 0000000000000000000000000000000000000000..81aa74dc4de3356a4e08f2c718061dfe20bd7f9b GIT binary patch literal 14341 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%0ASBCcObGd5?`qN0Z1Q%qBYm$o{^F$hItfd=okDf zRso5s^tuiy9xd}o`@ca51S$1^g-2Ej+t$)}oRwgaxRp;)vJ*oMfy}rT;QK9Dfr_mJ zUCs@CydYRRuZrpMvpb?zc)1%upkcs#cx3+T8Y+>$lKh4Z6US%CM zvNzw_{tyR$jmSOg3l9`7-Mc@_-dU8Q!SrAjAsTh}-D^e_WSE*=;TyUkRfWZr)qRV@ zDvMI>L*36OCYLLlFO{3QpTwau~T%c*#R72 zH<#Z5a$1Hg5`)lzc|iw%uRaDqIxYou1~X6G?i-)gdJ5{Rw)t`iOh8(?JV0{IomuAJAx_BOAWl60-l>UnO{)r49gUyfTZP?Qh zfHd+|aY`Yh*GWxlg*aMrn{j9cBN1=QBW4NeqP{rd2&8psw zugza3Tz6t>O(g!*&O4x-)S8xMWt@L=kI$kuz?iqsY^XtDi9kkoog?}W{(&&icr(zr z3d@{zhs~#zOin}su%`WL%@0)E06XdRuvA6;nZMBm0AtW;4iCb~`&|1;>;S7pUR|CA zwK#xIiN(CZh@UWk92e38;>^4aPee5@GSd>bsAF$Q zHNmy7x>d#NuU&%|5qBomghSXJmteF%>oOw=YPY+Z4HObft$)S3NnkW3claQ#dvGj? z7r4V~A<|~C!2j1`0RwQcZD|IA)-kT&xE=5_-T=={@bCIp@Q((fxiR7ozGhHwKAHkt z!G7$$%y%$f^*1nY3u?;yqkcvJsj1ph>&7R#CEvk0nCxnEPho)QZ@Dvrf}ER%~+$2}jANDExIzkhwYSbfD?zNl=A^0n$$3*`Qdb?-sa z;!)4avtA##VhS~Pz;y8_Yqj4yivplzN0|QK%-0+QW@%h0cKY|8r5h;jEkJjY zHu{e~Bn_2i{eM}eHhcezsOWz#W#HX!DK_)$xY4c8z^6HLu?Kz>RO$#R@a{L9b{9^j z7Y@u1nr}A&qCb)<0cGk_RfTTJ9sal^nfX|HX9H{c@u{-ZKDzP0qRph829<#VfJ z1uYA~tg~Fc_knJwz}jEB^;QI!O!2VhRuFLz4WKap1q;W|!(j90lY%8cn;Ugw$M0~j z`#ms+_Y&(E(CfC4{=`%Trm6?8`L511$Qo+YjrWw4tS(Mw2C~s`bcO-v z4qh3_?x~GrUqsuz7Gx@?Y^?U^3I`R|zKuCD@QU^mZ2pz5W}N0OOv~987e4`NG1Bb3 z#smOzGw5qXOj`a4PO$&P1o1Cs_)i5n=p%SbizbK{GptI|&3!tDGNj4%>?H8H$K|q= zLD>8k*7X<548XlLH@Y%i*h7}*m#fQ0%ouu?XRA70*5|VA-A$6!DnKG%XUo`!LkI1Q zqa;+b!$HW%3t99jj-)I!vD?kc!o6jtNEH8-f~w`Radyh*EDNjJsa4Q&`B=vJd-M7` z#O{3szLZ-6mGieRA-;8RZ|VW3Y~AbQP6g9DHv#pxUyIl!0BBj*WYG&?eBK|;!W2;? zM%>v0;j@MC@xp(D4E6YH(=El6jfEc2=jZU{#fgU;q!@0@FqawjfXF9byO!>2( zfZ+bE5I28Z^q*eR{>Rne|1$hgP(^L zSoD_r&5~zkW_YTGm0~5Px-aB8yH>3)YzcX9xaFqQRJ~MQV?|mj=)(>)!Yh|PMN!y^ z3S}jup#1v&!$LUoT0bR}Pc4)B3yJSz%|}2H)%C(68T52P&_>3irmFN()Or_3ALN79 PEJW|%(F3{rtwa9@81|s` literal 0 HcmV?d00001 diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/common/media/light/keymaps.png b/src/vs/workbench/contrib/welcome/gettingStarted/common/media/light/keymaps.png deleted file mode 100644 index 784f97e43145e6fcdad1e0e4c480c8f151d74292..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49954 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 diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/common/media/light/openVSC.png b/src/vs/workbench/contrib/welcome/gettingStarted/common/media/light/openVSC.png deleted file mode 100644 index 95610a5ea4eb360ba59c6f0355ebe1b0502e3da9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17144 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/common/media/monokai.png b/src/vs/workbench/contrib/welcome/gettingStarted/common/media/monokai.png new file mode 100644 index 0000000000000000000000000000000000000000..ea0926178e0f8abd66e0665c996ed3b7cb8b942b GIT binary patch literal 14287 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 E1BYg#Bme*a literal 0 HcmV?d00001 diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/common/media/more.png b/src/vs/workbench/contrib/welcome/gettingStarted/common/media/more.png new file mode 100644 index 0000000000000000000000000000000000000000..e1b741da46a12c56dc4730fb6a38dac1cafaab24 GIT binary patch literal 17121 zcmeHu2{_d4-v1bbv6Ur+Fp*T&NMs$Uw2&#uP9^Wnr7ynd;5Ox@ACP4?_Z&)vpVc70xS>+ zg#FaXW9K0dC>{c#dj)3%Em@}C?;sGCZhLKQ)G2Lk1nRo0t-X^C1adO;Ni-8Gs*@Kp zNlR6N?>VOOOi4?H?!~RI(0wPF&M}`tJgazk^PYu?`Nv$G=uI9Ao^x4J$DQsnbU*J{ zy(tB)Da41dD^4;AU#=})98QdlpRND$A+=|DvhD)}CF*!sz+K%3F-`QIQ5Q z1;Q9cMhwiYEUG&COlMQmBgi9ns*!KdcBI%!Uiry4LF+WD<7SA*vvipAoM-tyFMojN zBOD*fKG_dJ3TcfG&+cgEEDe^kxU=J~TOZsmVmg}htccAaFQSCZo!~46h>!dOiCP$D z{8mTo@|y0RZASzyON1h&w0!tK1^RR9W8@A^SXUnT!VAi(QUh z8YM5OS-y&^UJ>B&e`qXIKOnuN`CvHcj7!9IwGf{7pG8fK*`9knn)Q3Au5`3Ma&Gk8 zkQDFzl?U%n^7Cpo_#ct5Nnj|(!RLDuMjs6&?A|u;dT3vP`;gRQrs{}nCztywMtAO) z3)>|*iDt^HS~~MFe;_Y0JTItlZ{U;A6|wX6cB;6wUB@|kzcq25Rs0#2+gF=dGEze$ zX2zuVrgK!@PV*blBg#I`lDJY6Ma*TsrS*|pW7;7oO%#=QCXV~<0^jhh*Ap{q#`Bg8 zGwx@{!!IUZ=9N{uk)X8)&Y;V&{W=Ev8M6Hrb$$-# zmjA$2I&}3_SX7t-L~Ic~9_3TLvm}~hh(=tj;

yLTFQ5!?g;)mMt=nK!^!Wn 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 encodeURIComponent(JSON.stringify({ profile })); +const imageSize = 400; + +export default () => ` + + + + + ${escape(localize('default', "Default"))} + + + + ${escape(localize('jupyter', "Jupyter"))} + + + + ${escape(localize('colab', "Colab"))} + + + +`; diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/common/media/notebookThemes/colab.png b/src/vs/workbench/contrib/welcome/gettingStarted/common/media/notebookThemes/colab.png new file mode 100644 index 0000000000000000000000000000000000000000..3e54f9875fc0d2e686ebbed1051fe2ffdbce1c69 GIT binary patch literal 2473 zcmdT`eN>az8NVSVDiU#n5``&rSlb>?)qxFG3Iw47M?z?jIX=T$4xk~V$fssNv<@3v z$gxm?P=pjA%@D$OG!a3qIs%Ca0*MJ!1DE7H;qh4GK7vhnN||lgpxA?mQ^1l%Cb;+(Uw5 z0{-wJb@XzkPW0Dl2ew;4C1LUaMvyV~cueS&Rk!g2+3ig(NpoL+6IzAa=Edn{V|=+6 ziIzjYQF2?hvwn!pEqdDML#X!h#A3T*0HCMGEC(Y15QL{90gf(30Xkr#p>z9PVIZfm z(6hA^0M>4zYpd&rV*q0XQR`K;W6KGX5>F5eB`{y#oQJbLv#pJI!M|`EYq&I_(yNYSCW`+S8w|j{tbcsVKnEv8ks>)QBRZYl`yjNPrS3h6 ztdG=OA@?qJ#}~GDeYbUHM#V4pN|BVs3v;p;xF0D7#Nsc#spDn!_4PKuz9Gvox`(eh z{DhHHtX+@US(d+KU}&@VWvL@HzKyyq0EnF&W+FM^>$Dq&jF-%>!gEVLdnTt^GFW+{ z5uTRyw;a&ICiWJ0Q)cYxgRXxh*qNb%6=V?vTxnGHOMuY zOkMInL5Hd5VZPK6%YIRBOuXO7t7Nf`(yH||p*eXuQ#amiiP4;@DOlu6<9u?oZvxzH zqi&*qbTsaM7eys7iW!8trIB&&$EFYV)rWq{|>MhgzrnF@lFoV^8_^-4J+m06n0; zJy;f+)^k)zLWR8uqAOk~AMb0MDkrT6plWvd2%EN8Pq6&Opkm#vvH}{XV&n*kYd|7p z4YtKHZzAm&i_Fzn}S$-9I78a`wY7Tp(C1ZC_tsTT4p>0JW|~AIi`5WSZu_ zw~XyPW-%|%%|5tk1HhPP7CWA!kn>2?EyD>MMegI#&3`Zb6f$}f+l~^M25QSowpgz0 zey`~F9r#uRP#i_NDH{|3$6J14Hr|-stdL@Fp`a8B+d&-@44>FzoncTrI|{>^Cc0@U zXVCz6*FG*)es62dd&h~+7z~8mf(UoP{;f6yP>YKIh(FRTM8|^Z##+b++_x zAmGXi3=eKD+2D--0z0p?yP3^I_u8Db2ZJNJNCHv`mUFd!h9!rmg8_5Y?o}Pmz(sJX6X96ie zars8R-V>;(iaa&HUD z{SwKcC?gS#D}zlHxCgB2&OhJw6I1Vw0gB)HZ}=3ExVPd*BEjM+JS2#s(*LI4zewW? rF9!chLAuOhz65Eav)Cyy(2TU)TZ2@)+#7*6D-imAcu@2H#8dwSja|h8 literal 0 HcmV?d00001 diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/common/media/notebookThemes/default.png b/src/vs/workbench/contrib/welcome/gettingStarted/common/media/notebookThemes/default.png new file mode 100644 index 0000000000000000000000000000000000000000..301bb1129f1bda09ec3ea5f6c5151689165c6abb GIT binary patch literal 2476 zcmds3eNa_-BO)^v9|pvaHfrJIt&~tgC;+!EpyxiZg$qI>BSH^lA@mvo1R>l`6ySnVdk8Mf z9U5|WH$=8F4SnTM5&yb&KN;4@? z;Q2ytBbyaKA33BO&3o~}cw!e0cf{M<`{BUAv3Ru3#IT%madUNzPt@D2P-2Ua6yPb^ z*O!alpj?-gb6YAsl-Q7^*lql_zt_-vB9qi!eFP6cAnnb|RyscMqa>oJN;y7m@IL_e zrVT&8e?~~8)KQwucN22Tb`NdRd*B$CYEn9`Xyi=hR7G-SK|#T`^!~;jXf)auxE$1a zS98);k^v{Z+tUrciQ8vQ;~Sn*Xw$D>C|Pm+H~qkrVM$^^DpSyyGTwI=F2)k~NOkM! z9VJ1S;)F7{o~yw*)VnD%6B3ZcggU!BsKM#!ECg`bOb@v-7j@adlF#o8aCHQN@X5(Z zA+e^WCQWh+0`A`mL)BSoX8$#+@lSC{n4LQn#=u2inqM`tXXkILZF^;DK3LVGVjLP= zoEU7m;D^BwWzh2SoJ`7d_N?*d?f3@+1J?|*r!0)dQ!~{gli^-oUTu{LRTPytEYl;9 zb_0M;HlNS047xk2d>c|>`A9EvM!jjxy}Ys=+MtC60J57S{*_1nX7Nvi`!57ZcOV7n za}T0($7U?e=|a1MRysC81SCH8xMQ!R(3p>Kva)$8QKG%;L`GUa*UVKozO-?8KJb?fdya{Mf0(9IT@7DZGfnA7T15+M;@X_6DrlCGLg z&fN4L>gc*Soq`6@U&{AQVql%cl9Q7^5PEfmjhwD2nkvuSDvXj5Y7yYT%)Etl66S89 zNWLp3m~HOPCb0DaxE1@BZhvvADmr9WwXS2REx)b3$iHoBiiQ9^2U0)Zn={{V9m}?O zYm3-Yt0I@pY$oX(%Q||CebVo`7M3G|N`9DFfP0`7!Bm2Zj_xe_i zPE1Vr`THLRKwxJ+rpmjQIrQ-Ig66$9ux7l0(XoLF6wJ~DQI9|M8QJ4WqNs;C-skZ> z|1E-}tfVAYwhaO~^KD8g z%rL#OY;i{J3TGWpwLr^>pFx)^tuEJ3XuCwmIh;lSwT!g3UVPUba6_#IK-fgP<*2%3 zg{*~cdF63`P3MZ#-zUi#X-_CT z9`EA|dU0hv`-VNx`Ty9O{}>bXq- literal 0 HcmV?d00001 diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/common/media/notebookThemes/jupyter.png b/src/vs/workbench/contrib/welcome/gettingStarted/common/media/notebookThemes/jupyter.png new file mode 100644 index 0000000000000000000000000000000000000000..a81eecb1b80441e501d7a1956ff94879facb6206 GIT binary patch literal 2489 zcmds3YgAKL7CtcwC}OllsJx=BTD1y{6|qvnD-f8a0U=uCVZav%5?%uV(j;`MJlav( z!iYc-0h@pfL5yN01R@WeA<8RaBq3m{v?lQqsN_Znj~+6!7Ry=ES<|2Wk(+z(x%=$B zzi)qgpWFa4$!vwy3IKqapD!^GfF&|`|J=j~UVGcTn&98^1mDBS09b6A|6%+BH;%!V znB+i`C*XD2DB;7>D3ARf0MzH1PDd_-24p{?2PF-ID8w<8P`i8Me;-X)X{&KweT7H~ zpuEU^JL)_W=r??7gjgnQtRUOyuzToRp0|InErU3cxjKG}pY@*t6g6bV*tY~u=hf*z z#wP^4iARiP6J_)8G(Wbsn#`5#06_F-)$DiB`v>G`EO5{4F#?&O+zf{1X${jjvm3@` z5(j|E#Zmg57cWki3mf9(hX4ppcc3d?qKq^PJU>3L82 zb=J&OIZGWJJJggObm`;tKnqJtQy>Z{pxy0{?t4WP_@LuY?}>kUP~wedHpCAPF}hU@ zW8m)6;iVUShF!ui(Yn^KArF^Je=HB@@>g;RF%TfQ%s6$b+R=u+9DwM$wCL{cZp*Bf zQ(E4f$$ z-FbG_l)16(>7#?|(lWxZuiCP4LWW8>4^B`inJXAv#^>|TqOyj#Q0Qbt_~~TQ?Lxuk z-{#9e`Yv5U+M)Is8;2D@H1{E-=FMfXKz+nPwD(d`(bnu$w%gRwItPhlW=`xJKUDC( z|D}SCj*gusYAop4wGh?`bEkLn@NR?vcUCNfbg(g{CVpQC^)wdzt*y1SHOYbgH1E94 zo`fga?_IyDd$_euOA5t<%<-IT6qb(onyS+L5k8Iyz}?DTe3s40zF2r)EaJWQo5g5I zuH8#ux0W_DUW)^ww018Bi`BeRtyN2zb@c=Q_CwCHarjkEH*0ktGpBB7QXUzD%ek|S zrz9MR^&Nk=8H&$=&l6}g-u?Uc3kC)TKI6@e{or+*p`#rR3%fC!12g?HMlfkmY9n%+FhW-u6#=@wZ5<@4WqTmb%!np@cD|NGD} z{GBfq40P?GE(ZseJ;dUu!TDy{v6FAPSh3eZggc3^cZLNAPqOK@lk(xYaRGZykG8;m zIL0v4S_B@kxCm>5lxMfYdiUuqur}DolarH2dMimM5)&>w)OkECjLkq zxPC(H{&fg4c$-i67t;Hr#vpoIxgxILpb|S*j~;sKbFC$}v*}YNU`K z)r~fn(AdMD8{@KyS4bIlZmdCFbY zbO}^ukHxuFh}EO0u5Xph7P{t0QCXP-06j+M9;ZCZQyH)KFWuW;a#Ttp@s0>Xc;Ny# zDK77@vXoJ|kF{0k-j3$9@rpPoh3L||XV8$+qnaUp9RQB$R%>LO#}L-y`YberWuY&$ zbh89tv-Gj2+#EJ}zugTz1omu`tv3J{gKO+$DKX(#{Qy78VDt|I7f&CQIE!0bJ)vU! zoG#>V@P^GXpVv#e2D^6X{P_4^W9s)xX7R!IhuV>2>MBS@wgPZ1(4dxx2mUvx!i)p3 zuYFharm??h_96;bqO^c^n^N{KdT3bGdhsL5LL)M?M9~DhrEwXENDo@}YQr2Rk-2u9 Tglmr>2}`kagBjSknAmyPxcPPTO3qu~HlY=YM$|K787zO!3<_iu)sw^ zn8A}Xp@HUdD-sOp!&QoIkjefE=ux8U0@cWWmXEse&j`D6FB_A-| zt4u=_Og^5qVuo5Zj&axL)j>1srvDgzf>lnvbg!{j{GmtFQb`YOvuW`{w%#PWD{+jX6r{ zNpG~;yyH$Jq8bAr$;*mX_BAV9;Hd_yxvOQbprd|xqryaX;jwP_d@a$V(uO}v zvucHV`yybN37y{Dc=42;v%dV00qViN$;CaNiSHEYTOs^7L%Z1&n>#8#6)ezdg2X|@ zlrJ&IH)Y^_+Shxfd5JpeMc)B1A4=`CLbNWGX}4R-4V`Ug-BX3|S4+yhq3)At`6?hm z;sU@`aR{7842ykDp0%aJxT9GZVvnoyQ?8U!5-pwEu)M{E zxLE?5Q$)m@2zX_`0CsV&zgkq-2FS6_`kg^INe_|k$cLuG%?X`vAs$4?BMJp# z$|WtJ73R(b4z&~|9e7tay;r3JOh`{6lxgpfZZ1wTB4 zOtiJ;!RDp(f#IYm%Ey`m@5J;2*}Vrw1+;;Vg!CyR5!D4Rm4@@(Y$1WXS5zNfUt$^6 zvG#gDo-K{0MDF8^PV1XFTclC6U!2*(%{xdej6a9BWyAVnvTns8x+Bv!O*~yVh1q^S zzkn@n0omq}!@%F%Rh*C6#_ysi%U?hV3&W)305W@K?JY zO0hJCav}hp(96yP-ap`kika-{7vwT$F<0esL9Mc4N{~;MCwy4I{MU(}s}!_9VoKn; zBH@#<+$HBh9DcRQ1F!TxaDkWo(EY_BMDLxyT9XMBIPHd-5)qW2A1{eR5mVPyZNUub zaciOQHP13JjsrVVPKdB6dHOpUE26Jhe}8H8*kkBBNs^_j!LR9}R5w$eTkZeqA3b>r zX|&=O#Ps#{mxt)yR=_|@WyP(2{B3jO8T;Mohh$_sgo82$RtbxrVA}^)N(@WNuWFvW ztPcE{>jKC~tG|D)g8kT;*n(orQ{S6}E`$vd?P(AY3zK-X7X~ULT|3tPXr1cTlqW)6j-|g6VaYV)`&q<%}^Fy5wSj)C}aTk z=BcKn3sAc}+7}6H0z<8?D0n=koAZdTC*~km0~CWvYhT16R#fS(CAN*jONF0cE?k3; zEI_kgEeopr@uC?B)bTf($v^|#Srrbj_pxHl4rd>#j)({1&$FvfFyxefB7cQt!#>@- z5&q@?VSyM+cH*creHvvXPp6Slyg>X)dbXsCTJ={?u`TeNlx+{pO~aGI1b_dI`hFq< zO}q(jCY$B`Dnk5hpZ6Jf%tTXm(%LEKz3bqeiWfYe6U-3n@sM_)0o*Ls5bG*bF+K)) z@5wqgRJMGE;fn__%FQcbi-xXd*-pVfdBpPsc(Xy`)iACv_?F^Lw6UI<20P>c_bpma z0zbV{620b?79a=p>N>&_KW3i0;JUWM3zb7h{Nta&3)Q1~>6h#hn+u26MoB1b*Wq^D z?(X$$Yx~c7*l|uqr<7cgGc0B1Cf~J|({k!0uISGYlRckrahI!q*HC~aeqXeUJm-+( z2CFTZ5}!piYkVi_?YJ20T<0cBM{!?ViSIX4`n8S8&E^Wm_wh$&;rrq~eU-SUdVF$$!qU{39JG#iNQpKv$y4%L%_ z;uF~Sj)-DA)!)ZA-Yc=O-ZeuVwQ%UgYj@TRqjadpj`3|q<0lNo%?yo|FykR z22Sja_zJx`S28;E?4U>8gKJDvF|_{;sv4KinxfA?+rjUwgEsL^Sidg&|5P;x2={vd z8rQyrAi?tg=@<#49_d>c`fvjByS@t010yO}mgA`bg zslyKw9{cEzs{fb%9gNzF!8)2!VSACsUfz(0gpL&DE1=X*)W`_p=O1aTdNK~;s)F@i zk|_Z~p!tumOfg&aNg(O`_8l$g9mXUp`B!MyV-D{NhWg$#uDIZW@B$t(|K z$Blr18Kn{5^ol))_S8ib)<={#p{lrdbOT9)R_PM_^DyudohIrXW8JJ!&IZb(pxX2Q zJ1~YC2P_0q{mKT_PVgm$V|;}f9R81dTaN?m+qr(K|KWo&9K#lw5jD%7ST8P_-6`S`r3$|o!po{y?l*vR-bgX;c5XVS zto61O{yMS5dSr*Mf(M&KF7)^5Yga53>vec#Srb8n;m7Fgj{Lu?lYaJl3`(YKx^uoo zko@5#<_{q`^HI6>E2P@Z}Ob`Mg==ohKk02XejDQd}ZlJ@K_b3e?=z7 zA^*PP>mXb2FC~^$rgC;8bxb<`H6%QeQXf_Fz85Y(OfkCoBIgi6AJ7yT3sb-frv=G_ zp%saL3mh|4w8a$gTp>K?TG<8SSR^ebJq{-7y1x2<;A>5K*rYj!jux5H^dkBix}jd2 znMDc=BY zkn~S15S?=#=pJfg9o?7{HKSo&>jl30kpQE(d^ZFKd9gxkol8sclviPzF`P@vz#Dqq{Fe>C$`zRk(-|U%7ZzP6*+FzW zs2sQ;PC2oOQ8QPXxg5~e$74FYc-j~9Jd)&~&!_XpPej!ocn)m$c|Bbutuz7A4{H(1 zgoz_^gGRD2;WQ;Uh6S2Go~|O8w-M>+zT3D?5oNie#|rJnD&+?26H%qOOkj&n>jO>N zjP<(qs=zk~IQp2gw#z{VUZPbx7nn4CH(+VKh8ZDUbt!u0hB4}P>RIhx>bs8;5G0EXmnQfoXu4bKC(d4_I z))kN>xsU6gGt9%rwqjsiTU}O4i~YzaX*XKD+o1!|jUx5GB4vknvJ+EG*4zEaApCX& zve#X<0H8EfMr#<h#rkPQn?fH=hN?fKWbYh=|8DQ9r>>qK zQV@f6Bm<^=*O=K6+M6j{LMZv}uGb1)q20iVmsrgvBIISEPa|3@X*VHnU=b8JgHVNw z2rq2lRek;w@bb;_&c?n4naRr07PVhpm&EZCS;7(SOExPnN5LQipAX!KG;m{FLNZK& zDV%opt~o;(A+$O7)^)~3|CVQw=#M|%3|J<4(3lKV1;*z)Ym~5l(Djw?z<14B6nchT znHWWB^Cyc7I-6P4U_76t%kpzSlxXNUeDDJSzO}JleUrf=uCeS~Ss4%^@$@|*UCRUt8S@A60=?an! z_+{W}KYJ6JA(U*A;}CK@@pzjd|0l`2p+mClbYmVk?bWnlrr6OBeWfq_g+}X!mI*Cd zObSXCUu{3<5xmg=Y(KABc&FC-dd9f4^%Rein+W1K)HG@WWXS;cR^xqf=$n!o&(;20 zxF8iMXy;Y{Qs_t}vR?>?`35-S>*m2!oDZ(~mh8J9S{j^_cDM=D0t3{JrAN=PT~S#2 zWWwrsFNgcZa`P!GhVvqi)htmo?Sjes>|{EDGX^s%ds*sQTD>!o+PsAydo4wqlIol9-FXDC%$v%vImUUK zE+O2jNOpPV%X`=;8ighC$U4@Sz#*g8k}>2h60TBJhkl#^-QMM^KeT^S*p^fLwWf4Z zv;Ka8fAsy@M1$)nXXQNPsx^!Qg%SSrcd{$`gA$2LyD94b@MPJO0tGUMQr8t|>|oSe z6}3NoLWIJd)%ETnS~M2`4Q2D};G<_p-=ZF)fE}$x8y5vqT^ua`HnQg{$BIYi`VS#k z5cFG1_`^MEmnrJ8z^}Q`G=S9_a?qVF90N7){&>xT|M2NKV&OXsjSSU8Y2wb+PEe?m z4YEJDOCNZv=wbFr%%|kA29CJ63ArNBGl?zhUvDN*e0mLVY-@FTWXjWAd2sKuRLf}FPk6fh&KNrJn(0S-c3o9gavNHW)+4@MW6;tOxDp5xug9f_znM! z7E>O|k5$gt=q$O^%wFr%E({I`C@@rhkKYyy@tZ`H1=qmh&c?pfaDrF2zQ+^pr<&V8 z$db+W$OFFoko8bkIa1i>5zp5B{9F&GO9QZ~O7rz)~x z{b`za$qBZ)C%l_EAd4*Vt(a+$XpHe($3tOpFC;255(0f%({+Rja1><5Lq(>natc}&>XA!_`UOYub*p<<|uPjwt==X%cL^dP9t$U{jk#|}NqwEFqS1%XF*|ZvLY&WA@wJDMZMdz_ z#ir_(k6#s*ILDt$q@JZun-guUpYwXsoY&b8f;N>F7-LS&iJS(}{-lY#>nhX{wXB-i z6shM{-Y$tsb$IUA+WGqZmN$pFIi=CLO2Huo2o#6EZn%cUikI%pkU`7yoLbI_dU=#Y znP9fkW~XkL{i}O6xJ$_zEpbKTe%=t?iEScB*}ivMv5WzFH1PX9LgtTe!e}e49)m!0 z+;@L$M*Iw0#-@0y9FQ_Lv2gb#_`5YJEVZA8A<={RWfy4>aPyQgHS$rFSqrW$O`=r! zq%El>j~vNIPbp!_;c@^TS9vnJL5^4Amp9lJMH%n&1>#j+3j3iMRt|?>T-DtfF7oym z1iiX3nS8wO_pyWdYn(KRK7~L{ruQZ~gGX^-OEPu1>LbysQHoeO0FV29Q!2X7u;pZc z-3WKa2W!NI*N{@b@~G$9)Zc<1|IR2dhkIJX0AoP3J>o_8PYHX)GXpDnYu&*eyCq+WJBN9 z$XA8DZwqqLelVhfO{3qJ1AV#T8$s!$9{5p~=Ur!B_$s>;CfxS-;HFl?HOwz;XsWY8 ziSWgDg?xtb<<;}yrbfmpgjPRd3yxP#j zcLlyAGAawG^li6Hf41$){^&=SZg>*6hzm{HVm^p4Mg$uJ81#1+{J*1CZvT$RgUJ`M zO2_=)M31ofQ;S8b`^L^*m5IybK2d!w@Y1bN)}I*c+h(}Gu=p08k2Cj`KE2Gmo%1U( ztn{*( zywC*pi7(J&eymNWj6)h2-N;P%ZY$tvYzZkr%*q;oG37E+=6^-1JX5tzVPZw~{mD_WCHtqQ&;ZFQ=`-JO_~QY1gHJPE3u!i=6 zThZpG_BUVrL_$$r3Ah=*(#Gi!5iCmOsgD#Tw|pLGOXb7Q3w#3YJHswA4>=C87RRot z<_HISXKP98)wQu|G;MTcDIt2`W+z87$lp_D26($9^se&7Fc#RUn{S)haA9YZ5m>d9 zCeN8sUF#yG{Pk#vO;v(3dUI`Jf1l|`#IC;GJ*jpsH*S~6+*9t>;A%$Y<(<8}oH_A= zdi8184Z(@>`q}$;kT;OM0)=#a$Z@Vkq-Qo0GKqqS;KP3qDG zST6$0^o;=%DUHZLc%?d5ebS#kotxeW2AK_SBG&!|7c9gKtN3~tMjLd?VAL?R{;?|!M=LLc}5W6V_Kg2AshR9>kct#clgZ3 zOf>-^Fz*LnZljGbRzd@KfmoA>P0OIL%Eozg0wHJJ!@TR?o32Ed;5df8Eq~E#4Dh;T z(2P+A6JD>P7VFAtWP(YM>!D{vXx+zb84Ns)%lH!7S*qt4? zOXm5zbd0QnJ{CLE!`K+j2(&1-kCt1i zb_`GEqZ3TwIlUO$zo4`INdTmyYqzJ*bKjS$ejRLiycaKaU+GrUWk3ip6~H8BH6SCB zi#1f`Tkc`z@HK15@6gSQLkZIO<3|mJ}X6Oh_Gsd|-i%H=tYQ^FS$t zAv#6q1T#c@1gPQlu7T;u18n$7Z}L3Z4N$iN95%lpU6YSA3L zM5dD_MR3~Y2^aj<<8EGtg9uegpKa0VhRJl%=H)c0NQ&AOu9 zsbfm6mJ2oUJGx{M4aL+Lunl=wX&-kTz0bB-%D~+vCV3MKQ)i+;;sB!GmjCy!SqBcd zW8|-&b`9n1d#BgFvU!iRnT)~m=%6PBHAw-c5f0~!;Oxnj^vSQkE{nK{d@2zl_vo->>dQzp@H1aeVT*i zVfO)eVBwTs7GLwnpI??+Xh8J&$1Xdg_b5OMuF=NI-am&tmV|FHfWLOHU)8s)Ajnb= zI!|lFTV{!|!QSWu{yAsPVx1g>)P7~5wXtcoF{vsMJg(!1 zVM@Z#<8x|10LJ5}kvjb}mU3LjxBa5$K`Rdl>q}QwwF3?v#oeiI79Ew@UB@$gUZSCn zv(dY+RS5ZV=@4+d1Sv2>97MWl5V1fZrzeN6`^T^;NwGy9qM1I%w6~G}^70RUlt)L!f}#9`28q z(F^NiiVDb>Wj25458%Da4=PfZ7(!etbYsNHBv-r$P`JNasfmZ|{4v6z96}gelt`_u zXkw02J?#2LvobED`!Wv)$41M6Uxr6$b^nnwefB1|UtTB8&ElABUfg3kLJl0e_%bu} znulV@4&W|g8bQEKCJ*hoMRsX}cK8d-g-M2#55n*Qznj8#$&57zyd?Yq5tb?pUm|)Dg#MF}Q(K6#lsf5k3QeHV1{~Vqy)L!Wx{0{Q_ZP#h8%$m`R z?x*1vxsyXhZEl-G*kg58zFHv?0-Ya->|1a_dt?KluW=!<_Q4q0@@0C0sNbb*;zG-2 zJ;SWB{X4qc2XH}m^iGuLJh~r=!yDX`?ng4e^^43Ar}a^9%U8{mbdst1V80Cf>66Fn zqsEnEbMbaFZCPTX1vB;V(t}@XX>7xYV<|cf?otr&=GN+Wuj5Qz$3H|8@a+9SojGt& z{sM(MKccY7JcPkqd+u0(0(<1@&=11d=qfFnrXHRyJf+`Qw;DWt4cswUPZbJv2)n-$eP0sQE&6zJZ9m0AR&$~aI z;FzLOFQAXnuTvN3t0N)tHd7>Pt?V&aq)Bs#Nhya*Isa(*!774M9f8XLy80W0$0Y@( z!co_H*|ElL;?;W!CnmW5gUk+)iA3fc!sMRH6mv7}Ite~iWG zdiMOK&=dm*lfc#P@d0~*BrvrUv%qA6a=Cz;Mwd1MpTtPjSaGJ5_qCDHYpcyozwRA8*64oNd?WvmZtYXhpG?}^vL}EJ?4TrF!D~l3mEOkKSQ>pQjLRGuZ|ibF)#;FV~^W zC&LU8)s)NGY}$iEk7dBjH5uDTi~#F$JHhB<#xVxJB6mKbZ)oj!dqi06&IugP=N7gXD{&Q2(B1j{Ads||7@X+p)XbDEc%hc za7b?p0z)Kze>w})LCVGk>{q5!I1Hgplw*{^XeuvGa6P8 zF*}-fkYvhm+vG4%o#m%=f?2PTLfZ)lY27#5Xs&3K5D?3XAYgmf9`#4&u=lGp)ix^y zSgAW(n#Xbs;xOm;MZ2!a#ZS|uF+hnL!Ab!wv1^R9H8wV0SlN{35!jb5_>b9MwRByn zt+0Q>6#Qf1J4x%8p&fGo{d!8(&C3F0 zGzHB~D(AkqUyec=XV_xWtSal=s_J;I{VW>i znxMN;&tbS&NSqhlsYla|{vXnU0-3bTjbe?-JMbe<&72$Vcc(_@f%A5625W{Sl1Jr|IDH; zafBdSd?^NbnhcAIRHa4%(>AO@I4oR5&7!x^mQ)6O@~{eP*nV1zYK>L> zP9lRkuzVY5uMgXX-E*cA-;Sv<{lrebBGz6-1>LB27uw!&6hGJ>zRINSPZRicF=?}9 zuYsijxF9v`G8I*lhKd#{bp9FU%gC8^T&(WMe+D zBP>TFdNeo1xU=cGE}3F!z8}ET_S&}EJVI54A@qn&i9|-&pBfuICuYlH21x-4CjV={ z3aRosGgQkss5h6QoapTNZXHu^pZFiyM7SL=VH#o_0;YT$69!<=T4_xm{>05R6wqv1GQba+xe(k~|ep<-BT`tiSSHBwc z85tV~o&fD@Sj(XM=*B0mIwkOXH>pX7vLi04BOY5KZ#(mjJeTOIml%nv`wX`Fcc~vu zVtt9LiK`w~vd-Tlc-3h}f(_n4dFGRf-l^MS<>*vngA}Z0cx^q;j9R?k;5QONYU^wx zUT$!AvC3}vO3QhO4f4t@y!tx4_EwG8eh}kfRcG3nMoCI#%BWNW0zZF*0XG8(HDOuI zww(O~cIZD>Yl#U4gqpRKVqw~^%5-{lUaZY?lJraGW;8j;uwdGSDPb<;V4HASx8xa( z?Sftjc46SH_yRw7sn+;lCboBF*aW*bo3ti2Z#Kube?v=m=vAQ=b7kFLyuY+;3#ut{ zF-@|^GiR(D_)AMpgew zTm6+sFIlo9IBI(tp%{%X4A@OA?P5r_k26;_@v_y*Ulb{nVv33@fz4Zt`jX|Pl|>G1 znE8JArTltB0M7UXq<`g74YxgIoaLeiRTH}!7iqr+xLkp|+DM0kb)mbzQt=f?1f;+l z4ODat0&;V_058HZSE{=me`LsV(2+i(_p`DK-WJ&9^$Hr2qzBvvfn(;MH1pPpTeUOiZpHps8fmmPbLRzG4!4 z?%gN@Jy?a+MH<6|X!EEs0Q0!CF8nX!I1C`imWprKpvbRVIJOSuAMEe^rfk{YKCAw* zS-As)`T`*<+8aiJcjj*!ZC)34Tf}1do1_v~*t1D3V9AWU02PZ}C z)o-qHY>^=>+B!-24M`jF_$ALTE~Zyy^;gDWF?T?Xol z5WHVU*3n$RGGMwv7eg7ep*42;AoZ`{+Socq#>{etbc&B=7<~Qh!L5*IWm)fu0zN4w zcA*wMJ`nRZY#G8KE7%38=DvmK;n~D!3VK<`rm@yPnU$)9Lkid*1|jEM%ZT0L5E4FS z49{mC9(aaLw~Qvi;-}xZ>3eWezStvSAN@lK3M6>x*`hxbzUN3JCZH#c|Hcl(j}MW0 z45)U9c*~TX_d$9ks;f~KQHD|BG5j|)2%6pYe_9v`9`xB^0X;A8F=Ku2V4YSw$FV3R z-t9d;CL*r78If@Ip&OyNjIvr`U;wGGOSQWaferdU&UUeWCYTrgRspHSf_Gq;#1>qH zXz;KI*cUr!Az@liM5{an$~q<*&kl3F^6(x)Y_L~^NDS@BDZ?_JCN)gQ&i7!L9ZYZYtj(sh3+@MwnG$Wwt} zy;d*aaeO7dLaTi+b}wf#CJfLrqZKX*kipiDla2EwbJS3G5aQZOl{1YJBbMecNK1L1 zWhIgZZIz?aI8YUR&QE@}m6AF`(O&g^6fWvpu4$08;rEfZS(CbuUND1tr$_#he({H# z62BM)OM%E+s+zK&ALE9vj!*Mtp-5ANsbm?~(rp4_|9oa~RTqO`$m784@7xFxXHDCS z*mIG;1qDu_Bm};f!EO}M{&eb`*9@EBEaDNx&oh^&9yk`9y%JZ)X?bxDRGlMfq|dBM z$64Hl3<)>n%Avf<9PeqaY?#a}_}RyNT1WQ$q*Ko;t*3?_=W2a!=Sku{tcp(-NIZz^ z17~OCYREf%(YcL44iM?5t#$eVrX(L`{+=Jya?Fn0LoOS_+U}5Zr&FQz0c|+ZemDJb z^7|eKj2EBx9UopN&D@Mkf7L>We|ojAmaKyS=P*UKVyi5H$bgu6du zK2C6;A?H~gId2&s)AB^3s)xuf!X|2(L#k+&6tpmr-khac%&|mKE#9eJ!;pcNp z`%@S9*9SE=my06@TaL{?f9Zv-hPH=&Zx5K?GfniZ)x%3M`010Suv-4&yCR|7H>s_c zavBs3ihp3keVQj0YUM>MYzKJ8rOM4<6Y(M5;W2%^map|zCii11OPf+)0Gc902mAUp zFCT6nVftFngJD)4A7~%B2dP$;?H{BeWTs&Ur9WzRj#_fvsmeYeEADPPWT4u9fOt5z zDVjFYlZ-=li%0)G5V>sY#zXu=rnZoRAV+c++i$U;qO!a0hbu20(@ zaGKvSbgfmu8+W;zQ7j-`2}*dI&QBN2rM zL8{MX&uY`${(M$Fi6>IP~%C^8j>@J{QMg5Cs~wF zFut5(Slu5%FoO0xQP$ZOFz-IgIvd@&lXoaETw|fDK0#yulI`roAr`Bao8I^+A#{uw z=|faSC831w2gUpsv*7)~$g7c;yxu&t_(zd@FFZe0G zRV`uf-#L2IZMhP7pCdy*bdMm;AtJkfs|1>3D9a;nIcy+r;=iiFdLCc3Ard}!pCn+= zlGof@W19G5f-IV7^*3SDiiaM5JGw+}OzG3BiIGKZp+D3^B-{J@`f#G{_Fszp)~fg85PXhjynp>1*&`h)*`?**E0++1 zm^@};015k7FZ?hISGB=0ao>_v=`peqv66skMCob#wZQ@Tr1iHW+wGlPn9h+djB-~ZbQr*G?X$yRKlB*L<4n#gdH~B_GR?rf;Yqzjiti0Pn!OqD(HZ-ka zVH37EP;EsGE4K*kyn}X`qa3J=2!8GHdaHOpT{Lis<3Ou8+VS&gh5$Y=esZ(_2WPhO{E#ef2qNA^tNRq}Z%8*X zQYIm@fR;Go)c3sNC*Y-O=N1}r{ z8YPVpGGH?biz90Nx1CG!7+&#)7t>(cmLq;GFqHGyHR}_SvU})}Nto7BGy1<>>K7{M zG6Y7U&-H8iN8&GukE_#6y9~M_rM@Yz{4rD_R!H8uY*v)xU}yF1wLFV*dan91h6K+S zY!uGwgJvQ@A@wq1hP|HJm??G8u~3u?Im7?na=TdAjFn@N&Q?D@g_YGkbHGz?u(x~D zF+GpWt0!JyKmD~?1_{&{X*0q@`&Jiiw%GH+B&Pex?BBMNYR~dKvg>0HU{g}O^Q0HhmU-KczL336DG>8&xP*I)ViKjT|0TbV)6a zK}LtpH5$z810O8(`>X~0ez>Qwq%4ZC3fs+ZsnRi|F!HX+3wfxu<7Y4SacA4) za$S_iC&dP(MRyJ75H2I3;GC_S^wE)s9dNYO|J{!l>#H$Km3Q8&o^^F=&pwa6(ao*u zr)4W1RxcKj_A|w@6yAV=j74ond9JPBrs?64SQ^wa&#tl|&&&u-wp%1ZbS^J?CqMuJ zapG`d3<&s?JPS#a7N@zscdKhq$nKK4TjMSx6&v`L{&Nk7hCzl1mYN$`TOsb#B+(J|E$7aRqdJ!7GTt7dR$$s+r`#Knq9g zrq}$tCe_PNU}IV6@UIBpA>yjS(+}vgH5^MzR)YPZcV_0kzURiazIW`rihu7KuCM$D zy<`;2QfRRS-zeGezc2A1f&~s;qMOHx2MJoe2K+Fsp&ksNkzS4pSHM7Wp)AU`E}{;h zz`g0XaYqYU3%=$lVDQtIg-(79^!BNYN%KqE$3aKB>(9*%6|^hrR(<`B59WSNzIqMy zBKqBOtH)Z5fFG`w(muiot+2p?!qFU<;TTdXYojVgdr{o8sc2OeDx54PTAN#(xK~e- z5F$EMruIKE%q6+rqtb{-&?DzT7F>vEPH#6N;Et^Io=8|6 zuJih*pQ)$1@A8SjFR^Cvp|qwHO!ibMnfO1;i<C_j7C=2qQg>-h+{uYu zcSKgu68RI4lMsbhVe#ngDTtGYF=B#b)UZQC7=XtbWGJZ>8R2F_12-kd_LU}uj=Z$9 zs$w%E-#Q#7DULphJ`$;=M{0|7*XjtXRFaOK*T#ruyv&;9ENW%LbtnewX{sl4kdTu3 z7WH=GPz0%bXd8BPd?`+xD8>=>KC1xrg8XNVx%zikowzz&W@=XE8GD8d+NAa&D+gay zmRTFdyk{Rxa}HT!*BKs$4=!x=q*6AS7gskkDyh6hPf=%uI?an)zTA~+C%I3Qio5V0PVV&r zum*j}!_ucz7({ZG1k5ls{g;ur#v32bSjJ1|?xQsji%c80H#bJxkcgt^MAjbH+7k|q`(`ZR#+%|!gPP7Fn_h)v+qW4xBKS-3S0cP&D~-%zKC0Id zx{?KBy#DRyAiJj*L4qC#K+JeoREpkwCn-^l>*lv~>wW12DfzMIJ*AimW4z#6mPf-I zlCG=6^e3mPisLTFskb zc$L=I#3j)pwA|6l{?UQlPa$PTS zh-^m-uujiReY54tT>Us1*5&$?WXKC4Ju;V zN^Ow?vPLjFXWnB)Fmz{PC>t6(Q~=DudHIt2I0WL^*?0+`7(@FKdw%m2j)6)YLIrn+Gor$=F&@u05KP@q z%SDj%;X*<%n4uw=eR^2ou=kJtm%!{Q^$3-ygbaBEAwuk&)r{_l{Y!oh?0*8h{kaQw z{x7R}xdImRo4(pa!kg${x^!1NMY^VowlU&*zkmb>m*CxF(3@Dej9FV7XdVsnP4Qo@ zlei_)+3;UU@}C$Y$1;lo*|{!Fd?OCnITRC>6DI->oNv9LgN8zPo{7&cN@Uk-P^Ix1yy26U zRQ#zWAuAnfzL4(5@i~+e>}B#Vt@{_AE9KO)yN|GUt$nWz+1SY>GxDe}n)EwYH_tZzGm4+l=P)+tZy0QA zF$6j--ymGQ;`(mphPI>*uL_Gv$7a`%VxO3A_brs;Fa_5QPCKA&WZv8W#4Ej%iLA*) z#oy1qwgj*@-xOu%$FgK_jLeouesE50lh&>7cX&JADBiSOd!p+@K3ft?Y3Fl6_6rs8 z-{nL#dlVE1#CT-cP^3$EhSZ8Th+RD0)^5?Ftrqi-<&ePAY9ULP;F-~5L(TlZ-keI8NyASAmaz}g@TxkKN^MU` z;i$}~d}{!N`ey(JC>wmCl~K$wX%ZtDlq__@UOX@OU0?B06jg)so?)Z)?3F#@qGcqv@9XLbP7^SN=u0%NawPE#M07@ zbjTtp7M+4fEiiNoEGeBUjevl(fONgf_xC*S`^Wp4&+g2gxhL;E=gyp+d#3UDI(^g8 z>Ov&5F-QAf=*gaCy$4fF=UH218OM)-#Y?vDx2EMk#-&hNfHHDC>xg(Bb?8hr3C%XI1usPx}cpUY?kZWsqlK@PU@c-{G%Y?@{8y$(kYebb66 zvc9JpupAXYzQy`}$Av_M?L+;|1(%NlH@4NvdnXi)H^)kXCxE#WjAfYvcGhn6M%;f} zB*)REw#j&MrI#h8Wzruh`qPT}tkTA=py$;PwYZv#$WOP}R3JojGwz6{BhP4wEV+Ks zWPeCAceLka{QH&|Yu zh7?FodkZ;D$k}-~$HnXUWW1^(fxfex3pjjWeST|DeEL4^r?~T)&RXl`WoR-h*P*US zD{J$z3O;`uThL2@k`Nk_YU+OExC!#bU?Bf#Ez^+Q zq@zcmic49AO8=_xRjws(;PIJdxaAbI+Ng1Na*xqa+39n?WowzZ{=bs(;A~M-s*T!N zqaf-d+rUC&!Jh%I`Tf~aNECQsGo{>k#5?dchuVlKN22OKE>WA*$aFPVm)iA3^%}mo z2@?XRLp}TIOgW?xaq=NCiW{FeNWBPMde~j(a%8cmT6Q+CK-pjS!#TsLRkdu&d??87 zm1(I8@Uf-Fyy5=#R78?#$_NoS6*#0sSWhe%sD&sZA3ZD7^h@JaOEung%Hhu$3=ry_ zm4lfpZFgNnP=Q%N3)x_ZFg&e-Vqkl&fVfV1;C`zaT4!Dz%y%Kf9q@1uwtX!C8#Dco zcZ~9ZRnQ>+bzyXO8A~KqDI|$;WU&DT9W7$_!ZWmYrF?j|g@@e?Mc{G-9XK7adY%fe zn4QfX1-kd0su}?ba)^hn-@54rTi4r#l10BJe{z4C;cf?hRxJ+?szX z*sVG7&6N!qj>Nw^;~hAke%3w48fzK8}3@cDGrQVGb-&ID{Cr z6*10%PmYY^fMH<-z<{PZF*_>o|KJ!k!!fdB<89{=-hnWa>s|W@{D+V*%W%wu`yV|v z3~m$834rzg!tX}t|Di_+CPbIQ|9>1IWREX173& zY{Q}g6DG^LzN97lPiW4>Zl8lY3GfCUTs=hjZ2m{fw)pUT{7#LICcpj%&w#&@I~e&l zW9}5fi2V>l>--r;0mOh?(}18CW214+S*9Q!j0?iG$Bg^)8Vr{^^81wtZhSrc>8|h8+U6{`f@*_(@XS8o{LsA0g8{;jPF$-IgD`mt77KZ9v z9xa9LTF1NRw2ETqWCOB`4FiN=77@5DpcQqwH?D@RFs-~IL<|0!phEwCA^0c)(W?Qw zxHaDu>1mJ~v3kW?86l32w#AVbhcBqsEy(`m{@C%Yd8V%LT-G-sbZ1uLQv9ekVhu-L z8t(s0s=lOm^$pkDi0t#SGPK8PHB8=Wo*RkVUbc1c_5>KFLd!pQ8Az{meq_T?xSLAeEi(*dh9Iku5K0J&M#O z0S1Xl4eV{RzxQ{|4tT_>CqH++Q%K;ed`juJyuS}nf(t9#`I;2c)L^^Tq}#%$ubn4* z2Ex@lJ^oy!y4N$MBT{9}75*_|QFQFX_lGroqV^*qMnslW(i<4}#wHU^dlY%$Yeo&# zEKWS%h2XYbdR~dzgoSmZ?c``Ad{+bhLroQw|8_A;h!c{2L;)YG_$6r2VKx|(lgGj& zu%(Z3+Wz~C(3&}-r=AzNjolb+!-;h05sbI~uGk_(p%eY$ot*_y-0#ze$a@G|*EVwK zQyo-^!Vs_dsvBtNnlSIlFPF?B_foj-kmfgFQUfcVh2{}g5C3GqK1J=bp#>!uus-Bc z59XzEt#duj@5$#TCQWre|FOCKTW4n6b)_kXUb6;M@BW=1F+toAj|oj!WApDdo938? zM6R=CH|&@w$(bjE@(~-J1G?} z+B@ahMYqEhTkgd^lrIePJt+XZcO6tGTbudEisVOmsgKwRGUQx*q7!B!#OLsG8UAB==Hf1*7=$N&(qrZoAY8mH-99+FAdet><3TF z&_NMxb>3Sih;B|;r!zQBWbP4}vw=_upZpRf#*U|Y7cx8sWH^5mYoUei$MqZjX%9G_5NS=!*IbVX1>`oTjLc zqxS`<(0onx-%7lmSs^HLozO9a0F?~7?^%CIu6QUxQw`N^)@_Rd}}C zK%YL84KDQRsPv}8LQkbwV4;?*`{ zm*C;>ND`6tksgxlHbL=)sBj?%c4^OMqK#4Xz|B52+VtsY;6@n>%f=H<_l*>OesMG^wKVH#5x%?;8}mhyTHzzzAxYf(PzcLihBl- zn)2STh*E<=oS+$pclqyDl{Ki82QHTkP=2sx%M3k}Rp6R@t$wy0iI|I|S#_0&Pe$b> zd>4R^dG%_-zISKp!wwZul#*#*L5m=DE;es2Bz6D$0UAGj4LYwgZcyAD7goPH=S1bP zCQ8PwEb>@NnF|>yN<|Zmjw)Z2=S0HOGZ>)UZjP`jrsqqP(0|KyQTVPX#}yG;6mRB& zcB%b-Uu3s5!J!6hni6UfYk$fYiod>XtsKY=IX6Q^d<919)YbvL9;?AwN;D(mVKD*CHZPL=W9jiJW|_TCPng3k8&WV|2vKnP?iCqVQTgDv z)$mAhLq+lHRKS}+GDbJzJQY{ugW>`r@geeDi5Om{KA7=HNy9CY=2O1Ds!x5bQcLV! zqTXa-_@NoKRGx33Xe>x z##G)aYVMaUO32Q1h{#<-gx{lr?)?-Odg8-q2HzdipkU`7InnUaPnYQ#RfB~-*9C1$ zsn%e>_^@264yO22WSKpmRUNNEyLMl9>N(WSzExkJ z%p`a(oiaGasqHqs=a*fZoRPcB`2Mnt)8#RjUVt@g&zH#K$FY>)MdE?M`&pq0k7i5w z;U3zy`<9crVxsz;DGsNX*_weTT(iNo3$|!LbTJq~3Dr#$X9}3Mk&wOF%m!S)oWxKh z>b!K=vol>N!7N_?^4bKo<@&bqTxu{m?eq)e`@+bB1j_2quir-)@842#A@eBF%W%F^ z|IBsI{l$f2wh;Vk*CRUo14A_!X7F{k`1w&iA{$&+SJ1|9s(Kw(o;I!E8AF%C@9pVVPaT;K?cQkZhU@1xD?Tigfhln3}CV&h+&M`W-mQ3 zkG%XNDFmnCLayLD!nRxsfu_bw4Fkv3zqflHfYI4Aj7QNNC+iIDP)}WXJTg4mdnM$> zf7hNimogcTk1r3G;!BtD4M}vHrYnl0$`ZoTGii*nPfwy9DAxBzh6fV_YV92x* zBmoS1Y8mNuKkrV`2QtYvG^q-s z%z>IXG7!X118ur%s|ezExE$#9o>DW$DD=9O0ew?NjPy4A448ko`0Spj&kHc|-C+^A zfgYK<*h`C!#O6gBUX?{og^buL>`-?n5|tK3PTc`9_RINtw*$;|J^3-Ax^xP7e_NM* zCt^i5@HfwmEe>N$P-(1Lw*nh7&k?4KQPf-y*0A}^|IRuUJV-t`&21PETLS+=^PfDC zh#f)QJDb+#bT-L7X)Haf0u(X4WaZ#;p}rU zwg96G*-Il0Tc+#9sktCwN?$86GU-!+_q3H}%FgsqD}_l&jAru;P+eJWxb1G%83Cq; zn82=c$_K}hN&QL~gQNp{iObuK80j>5j9@6fY#vCEe(-l5xS2`R9r6_eG_B;d9x4Bb zo-syB5NNxZ#PCBICEE4w>||upn=^1{SgKHWC>NZo7+G;=tlFIA=2h}hhkJ_2l{Zcx z{-?>mO7Em5)A*vM#mrsygl3ocm+<&C){~nVHUB@cdGpDHCI^%#H9`6+HQ30PNUn8f zMoY={G+*UUi!(Qw#AJT-x8ZDcx81m1t>RvJKCezY8ngUd$~5W6ADbTVq103S+n`@aHDZmRe-*JXC-?=`!Vv6+{Yi2Zgd6k0dKNe0w|B6B%K8#B#h8OfkVrBru zMIAxF{yUemD&lQQ6zZp%4jbA2+f>$ez{vHAnAL~%!Bs8bkmbS2Fq$Ql0r0_xxT~^1 z0kLYZokjioZ1$-BPvlT_#d!&;@B4Y@9%ol%q#g|vU{75les1!5(?Z}dWrZ^NQE1GH z5Me2H%Px@xCRR{y&0!vr*!@h|Rq=-!ET*Ld^Ijb#8(QeJlghCBNU58X4@%tgWKa)# zaBnKmPcm|%9KwyWp-SkqV;|8{IWJ?^&F2+9^eG8gPk6x}VgRF?zogVd7e;F5#qRA0 zmi7xSF0@`sA^sgb6L~1|yg87pc`V@c&-N5~r_hDDzNJ26YYtIE?1dr;({2TUdj4VliR@VsZgVZbJ zqGOGszhaX2T$ysSABD4-(SfHSYnF44uw}YeUKuUDkS502mPZbC2VbISwkxNAm4cy! zxEykDv_v;vtUJG~(w4SY?8ZaL=v!7PgDEW+5(V39f@Z!(Lhtn?Pqs7Z}NdSR&p>R)tt!V1IHO% z1;*n(#qOb-%{I-#*V6SVC!RGC^Cy$Q1^n90>eOBXAoamrAe*U*8)@*{oz#`psW0## zWaY3q(iT%A>x>>wIvp|7Gd`u?X8x@x5+BxtHR|f3EnW|DIu=`e2-#dPm`dRl#4D_9 z*+?L2)kOd5UA?nTVwMrlU)7JRGF% zun=u1e5g$Y-R8$C0D4NIz-VICs7hIc&-B|!uD!wf$Wtn`4(H)bXd3W!PMT`BnHOGA z=_`D3XdH!c@2*Jk;|O*vCrU8RrLlKPG38uj(i9h~7enx{TD5op5%B;_-Dp2tsyk^;lh*c@;nxd<7iW% zr92)jMHVzlRjA-3s4xtWA-{-Z(8XzVd8SM+B1lb9>E>Sp>R-C$b5lVjJ2YTN5?ui` z3)8Wzs<5u}rxd>j@1onjcgmtj#uq`8ww1bZ{mdYfP9p2$y^rF^+G$C6__NMe$j)oa z`!T1ZiLSL}yI)5AK-(9N!IqNS_M}eEd5_@O$&Wc|j>|p+DBVGQyW&<%>{WmlThc4q zW?Hb@?aRjSjy=`=?Wa>#B>Rx+mcJTczL)vr(6ez}D< zTODsBnuyWL!sHUXJje`=*goX*vI^z1l z-XoOgHh#$OKol|Nj;9oJnd+bnax$!}GRDkQ{BSN?Bqf-W4Lf`2w$kffx+ktK)7ue} z4eujLuft@3Bg7mLf+G-?pu4Z-`wba1dq4_g~I7j0;aPtGzpXMzW!^9DTd01!Se{D6vlJ1i13Nso@ZHN?WI7ffWR;q zqx)O@2n6EpgRq2S(nai;l+7Le$C&I81R}wOLA^uAfG}+^2_Fo{{}`cAR{H-#{ErhM zcn_0I12E!4{x2dX_4WUW=(Nyv{t#Ne6My8fgNq?x5?b6(XFc3EW!PwVhlKQ}DK$EN z)t&iaA*mF9+Ze;EyFREHSeG$8E!7s;*mCR$$i{F|39bXodpD+o z9Sora(31mwzYV$>!v(q9Q(Jr6_>7|1gUaXzg`F3e%SYypfK-GJ&9=|gc$P@@<698) zx1VphqKZfMy>}00wEPj!f7eTg72L2_%>d8_``)a-uGQh=+}qsrjzE8S;n|gE3G9zQ zjV_*_(EImpx|0UE6<)u+^bm6M-JY}9gT5JjZ6v8m=G|79N{kV+k9Y{+!7NYVNibx2 zLb)mowt(I?hM%vkMDU_D>d7wA^ zb8GQa{sD1I_>8Y<@S(zDCOgL_rynt=<*>m|68^8kAbETe2 ze*aX+6No~E1M6K>C=)v>x_onjzkqZ2@Y4K7mXI2M<)GJIW!#do+m$qtvxGk@oHw^U zU-=%qmGupf`)T!j)C|34zFk)ykd$YSK#&^1#3s7AG+SQ^(wE6ST%AaXq`5CmkxYcA z_Zwd^gn;YdD#&ewR!kpmOB?zT#dmcHM&S(B&?kbKBadB4P!UtbP2Y0?$Fd0qC&s2` zvt|^Zqq!L)JWWL|vUipwmSxUY2;HM@N;9GAC0V=0@vpG})gQDUnG~e+`sp76{bVHNJQMap|Q)oJgxn@R@sJm>Q){8Ly|61nWQH`(xTD04CD2`o9eKk5WWDj z;9FkyE!$491A*nb^k_|Qm(UJ+CGe5?-aGl&vM4KR>9sw&McFeMR)p5In-bIDla{c~??NNHqKRLv`C zQGH3~`k`2vEH4Uj|CN@xeU|Tm7vh*yn9%9h0#T`h51!ME7oz*aWgVI(Z@X!>?%%Q@ z$}KX9#QP!ghl7Sor)Zbkj@YXQ$m~!k3@`cD!wfH%({IHps+F8b7Z1{#zMANL8*JW9 zHpXHYBv-G>2J++?Bz92Z-^A!y#2=!#b7#ac>ZJ@f+epqe6m_WBp;TXoiyF*?aCSc^ zKOL`=4gF>(s3oL*DE$Wi1&pQl{6;cD!2(W7f+)yL?A)HXr*i(e`2fG6y!28Fu1nbA z8%aa*HSaMZ=@Deo#IPMRXW8P?p_@C@eQl&G-P2JZ$*ki@Q?Y)UXBab3fd@ywamzV{@j&oLh72zcC_y>|I zc!56%M9bJ3;%zzlDIyo>kc1=Z2;1{j7&WKcKu!ff5QVC8)U=l)YM@j?&01FKSf0<) zfctdwYTLNxH5Pwkt~o1XMEX_Lu`lR;5;|GMX4=6>3C3Xk?<67rl(6Mqz`jaN$rspj z0uSEc6m4ZXzqhF@+M+Zn-k1$`>$l7XKdc?T)&{9pI*2q2cl)aiAHjF!I0Kzs=omKF zuv_u7Bqj3)<^!=Att`Vi0*F5;|H%DiSDA&X0*{tU{?UQqhIWsdwel&>HYKiFlZx{U zkVkeG2SU^O9{()|8VF}LG+-uPmQ!cW`*T#uS%kzb`?%D)*Ya^woA2e|Yr-YOkmuh5 z;8C+05J?mt^22B=3Y95*Ag#+1tOiTwL_$Za<91}$QbENDEY|zp+@qhIp*e>B4k)}I z8jNbM3{4`cB=u-6H;J5HVe>vz?j>>hRlx?SG+8KJ-6Yv=G9g8o7$SmdXOEj6Kps;< z3;Z$3yGKY^{2XL7$TaN%JgFjwQc1A`QYFwVmiO04A2&(&MD#^+VnW2EPCyr_;ZFltG zPUKf}DX)WGq=Ucv*AecuzQ2V%4C{2m$>w|at{nJi^k^gs#c;;`U7hFW{k30wncD$! zJjmRXAIOt0fsBQLuVvsajOD+D^)dIAlw(cjO{myl`^hKpaB^vWq;S&LmB%PONny2u zWh4O6fPr&qiZxbe?P22*%Zm|dMcfEmm#FTq@|~hXf_r?B1r4azS5!Z4le^wlo2Xl# zxu;IbLbc9GPkI*h%!m=9_L;Ew8fMVk_W6OR?CSVpv3^Q)ZbjN!usA7dYI8%HGz#6v zcZi(&|8PQvt}rD;9{lx*np!Ln7qM;D25I_BE@CPQ3lGqP@tCX&el}$vB}+Qt<*+)9 z#uE!Y(+_Ypv_Qn*^ExWQoT6b_f`ff(dJtWdV5RLZ2?*0g+G8(`8zLsE>o%2%lA;n1vduG!j|B@n_`y z$v!1taQUGkyp~ftfkS!IyBvl$;Hk`FOB_HjDJoxTD}14*5+F_%F?=I| zz%Mf1F6d)JCR&k0XQ;rHt`!p}TW0WMhZk@3dG!Zw9SJIe=o>OCwCuksl*rBe@OU(}L1mN3Nhe-;t*&*F|;^~nRo`RH@F!X*4rfqCGePmb| z?HeEpbwPK=3(sP(c$I}75?f+QD&Gv<$%^yLZ=j5tOG$2E?3GA84IO)WJNn!v^)o;B zj{v^glo^<69@q4^QNrM`T{9W^lI!`aKcyAlEwt@mItHG2*19liG+7nd>l?;Ut7O~m z&z8odA-&A!__A5>SA0CTPDG(Put-#_AS8_0FH971O#E8DQz^l3D%U+-2R7e%@xLp@@IJJl#? zlL?edbzouiXm!O@c-g4Ra6z_1YcxQJQ09Sv7(OZpLcYS#NEGJ3^)M6%HTeC%#0x66 zApFP*@%Kg);4eTJtUCJaVKU_V_|E{3&%&4`V z`kR*b0O2DDibVZQ{zQ05d~SMZ>)^H`X8k#F-_;vWtZYwRSOq?YF>*Iv{ao_h$Dgwp za_Ag^Knms#X=*faCrO-eC(b1WH4j7y{(+Vj^OzHy!VVUBXB&?B&b&LRQ{fbQa0;b6 z`0tI}JAO(HZ||U(cJyF&>N|=4Z?kI&3>JU$%EgsGUtgOi2(sq(*G|DXP!zgLE}qmn1u>BtMi)O{RWy*!-RmNMa> z(6gi4tDqF%-4J_$F=#tzTX1#X@hR-53!GU6Zq_un&@!nAJ9YhSqSdrX?k!`R?|3WIUboNqzEjq2y`k6Wo6)-=#Z0E)XlF!l$nJ6t+qYU}pNYsy_0AK>5BX z^2ohpP_Y2CEoxwEm~7o?Q!;K6l|WMZw2i=c%E#NxuW|D2HzY)4yR<}>slowBRrssZ zN3wm)Hs2`&J?(-`?bd=>A+UyYPXD_=aYtJALrrKABCiF|LDsrDPtqm zQw>W2r9?!zY-UN*e>M*>i}GP=`tCh5kExtZL*`OA#=5)t0`4C*_4$oG%&k7eChYCS z>I1LKqYr{cj$qN-b_9k=y+HFrbDmf;aoi>v=;tq20!bV%@r^y59&ippe!s>Rb0Ss+ZEjr%6d1b@!yG?b6I$4az@zK)zv)PHedMzWzhd-V0w@mq7GZNCA5J(je$1o zy<~T3PNZZMjD~wS;kD#jys_{74sn{>!Ax#79c9b$qiuIigu*G5@yF)BP980OyaaYj z{7WoFhN&=184mo#nocT}Tu9vQ);g7(x(@aYP4NDf6=5+b6jZ3MS9{4Oq- zcm;*dizTj*s8P+?7bZlHW?rlv$CQOhIi#uf05#U5rG=~ zUoav1FFFL?1rLnlV`5d@f6*oqb@6tYB{{05@av9PPXU)rlSe>w(NpV-Q0I-bcred+osWwTbndA~;@{}Q;ec$9F-*jQvC}bI_2eqsA$gz<5F7aS6dpc8 z=yVSr9OXul>=3I1Tf%w-Ipf=EW*}t;`_qkabz)rmG>bm@stBkWqr7YdIj#2VT$4!Y zA6e7F%S$7G*w>=}VaDv0ietz5w`GVTk5@mi7NXQjTQZ^NPfQ{K^cUc>K}vQL zO}p;7VgVzSn)Z=@#nA6Jg;S|ry+(nMj+~9k7TXl*(guzU9(i|PCCfqRa96G6(G*+< zn_C}gh@uj!>jCtv!GAf1OFTL^)t z$!p4A;($ZhdWJXz&1d{Mwkq@KUr%V1EI*i9@V}jZ>Qq%`AgeV%j#DOKS3~hpWQ>9! zAvy*jO{MdOYqLy>_JP$QrD%qs27+s>L^uFbySwb;$fx%zD~RHu>V7t4l5uX4);tu5%EN0e>jcWUKSMAB2kl2=i- zwoh=OZ}-P`X!-%KQ~k#)8i{$$VLq)Y23T%{-f3}&*R+NoMKJZO`b5AhXJ0*Bli~P@ zv{)l1OqpO0%o1<}V$8$ByS#3C%Lo@WPcil_Fz*Ko+FRKnxc;r-Gq;2FKO;8sBqla2 zgPfq+q0YCB#Evgv(Qm*ntIP_Il{pBn2ZHpuTI(D{PE!9BBDh$Iwo64JC^yhk7W{;D z{{4ex7IY2$DvFZRKvd0HfQ%69`eX346tQ|43JoE#OW>$u{axyPE%4=-k}Nz)K5^4> z(7%jmSbH@$Kr+#)nzA{}-*>E^V)sv*MdUG|n5fZ%l-N?a9i|5*X4Pf`Ca`MFl_+2| zg8136r0C-=ZkyLrp4&mM8wOs_TeejMs(1dfjYOHnt&;DpwTjQ2fDm+WPysPQ%;^mH}H z@n^AD`DsvaDwaI75I{LzZed4_6vL+|T`31X<1OuHY&2-;)KWVtbTImmA|v;2`akz@ z=&@#0UE|r%mkN0zSQ_!%&l8fRuTyBi@2nMkTbhhIXZavfp=t+yWXK63BrVS;J6srt z+h0GQYbJChO6JS>I7grHR!+7)WrDOeBSQQg>}`7lE$wvs2h>Ne&#B((xH0$aJ`~7- zN6kYlEBA5GZNb)n2PadzW17eI?7lpOfSB$g)ujgrg8WMfTPk*T|pKXnx(ZfkJVSAN|P0bhS} ziz5Yq03YXtxwm2XVp+eRokg(XVO|-Ne8g2WaxeKX)A^D1&(IUcLcoBl4v36o6{D4M z3!{mz6c?GRf1cj%*!@R0Ak>L;TUqDS>*!sUtgE;qjHf@dNj*$F>>(e>jpN440*(#a zD0)vH(RRzs1BoVjZB!CZ=_kZg_NNP#J=`t>h3Q@N@an|w73E0Qxfw@G{F<;@y-#9! z-2g(enp%RACH6yq`N9bZa%?)d=VjKwCq(LYnz$WSkOIGEnL$72!dhLzd~jYZ z5<)yOv4*vAz(_s!Hf)H!W37{bY6d#lSZy{Ckz^3c{cM#AwEi7AS>Wg_AhX2;4 z{pC(*O~TEhIPzcydtVYHPvT{ z`XgQ6&LEzXqY&aU?6MtePR`=;nw1A4cB{h@xWH! zox`}amDxrR&YzU~cPT6WJp9T^#``PTTpk^E%puO9sxZzz3Wfi0d7n{J3$Kksf@LWZ||PlKdeb`e_C840RmFB?1v#3%OaES8Sq541AQymD(C6>(`{5~kSYTBZE{$)p!NWg?yx-0z=PXtw z;bA$jfsa1-hhg5;)%KT|?xTHi?}xuB)G@Z1Y)`Y+pj_53}5B5r%nqvFz!kC&A(r3b<*aV@g~GO>|+PgoB# z35t3JK0l76;zo-*FIayHk`-$} z`5QIyr?>g;F(0JpN8jl?>;g>u5Nzm2uUU3XDq#GBbzuCqG z5S+w^8tf|HN*sCODKBXNS=}T}omd)Gi0TLLrGgzcrI=$EN^5VsZ|j!B45~J<(Jq%b z>d)tfM!ZuJH9p?{Ggm0yinRFCRCE^io^l3!4 zmz7?I0)g5&0q&_e9Yh@m!zTJ)wsH@`@A6iydY3fsP*DQ}#i94emOr;#= zQ?@d-ZPvNp!2z!lOUDom88?O7wLeBD8vIw@ zuivAAR*#(bj>sJ1T5{cw%f#2rCya4^5%R>C2z#{F2&TwBaij&`b%IqIa|oI|)2(jr zDDLmZv3&}UDSRsmdjS4TsS-b~wHvo*)h&dTtn;tM!EpN2Y?dW@t4Y()8V`t#!ZkKb zPNV7Vo3-aDOaUS5m2YtTv-gIBFFY}CwK3Y7U~S02czVv^fvC*;HZq8c5(R0o!RCta z*P3H##ip11kjipx7Jj5FwWf+r-w+4wDvr9pzz#L^SA^~tpcg;Ia>(_tU;mBH4rH7ORDhmXKgK%DZA$;K(AR>sLg#PQ>ke}d*N;RP}8 zXp&=I29?p=vTvI*ZWcX(N$f6}YmN;UL!6Z|kVsIc&hP4xqLCA;?po zw#ERf8j`~-2Gi-j__g&wWTqJSrWvSDy1m-&nSHgo0T690a(O=eoqXU^ImiNhS!GC0 z6((?KG44c74@UF5R(5-dI(%gYo(podWf|PK;g*)SZ#g_GcD#r5$>_Nh0GK|n^>}M$ z7NXK6c1QRiS>frX?V{xwNbb)Gpcu{d8n zRxqPwc!e|Vnk;6;M?{bDSyp>Gk<+XJtk4ffePd-x)6!!H46%G7lEWHUf>0;Kk02-_ zlBQTPf+b0R=?mVMhA7@=F-_I+x~KTEr%N@yuf(h?1X~Yp<@W>jgl(0Ub%a3|+dcsh z+j6U#|L1;@hhi=>ps{2JKQwB`DVd{=?C{J#J-uZf!$a2;1A;Dr{s9%Js%}iJ5%6~C z_lL(&^xw0HID?dx(c&qGiv`ncQ$<Ex|J2P&sVrD=5}i9$8H~8f4;l;K3p!g2 z`GE>G18D(8aXD!^B>0bh zZ!ztw#8(d%IWpVTl|dNa|L{`7d$c0hpfd|;!-X8yF@9Ub2V?3ZE)hTh^yJ{q zewNa)9~NeeV>Yn!0T=5;em|z<7MRU|W1_>LY|nXow|lc)c^gd7He293i{9&nnI^cd zny=0&`Mha~#ik_raEuQ?yOU*rOfakYJ)kA}vDP)iW+sC*=cDAtb;lG~F(qqqbXwE%TtJT@f&wi8Su z_`1afe14IdF?fDuoVtw#_JBGt?6sG`;&mopViR+ar4mg}2*A|M$Upz6KYtbEqq=}f}@Y!|K$f76ZENOHjd!*2IX#zMgM}d~KccM>S zXmF6?M!&<@$7+@*XJztk?;H;aXT~^{%zK59^^au&NXEt@53VM2l~d_w>rX9tM<#J2 zbdw8Yze*6rsYlq7wuLYwMS|(*3}0wPEMmdJH21o6Y1=8)Yt5c%H5yAz@Rt#8w^t2P zEaE|5QBM{9)-w>ex}ABvwENuOJnoS|o%5$q z2BDt`9^P_uq1I|@*!Yg4u0;50|LOb3Y!uIr4ozKdmqG=l%5p|pmVS#^imwlZFl$!3 z^Wv1ggFI|o%3kvBB{poojfHN00q*)xY8B8NuAAE}@7u7AOqe92j=Y z`xFnLIEa&4ji64uM>^1)&`btvO0>r*eN1~yQ`X8vRXFPs`i!^*A2@MI;YGSy=)n3; zGs+nSl@ZFCle?!2mj{zcVfX~cf23U&lbqY_??aFiAL5#QC12)tShcbs8N(5^f*IE# zermUFA3T0EZ2puBZCb12L%yIhKJHSfCX}WPzITseLty-GmH-wpT~%tfD-R#!xJ8hY zM7km3gJ6E?SapaubBNG_S-WG8dVYA|ZRhMSFUwcv*CepodrGV=;$um!&4h5|J_b4B*!t{A093fK(rj9l3j5=K%zSIiut_2Rlqt&;FKZ)2^mbD*tjCnmv zB#UO}VW*!pi2hoG_bmx_f{9P69mrTz6}!I{EzWU6r2d!n%E;q*^`;*aX7`ajz*++F zbLh?=`iBY1SjjIxQHoKKR*K|*a)0X0`Pv|f&7Va+{-#PrX*HtWJO6DkJ(Ea>OGo_j zmRgcOl$YkEC;nm(QA=t1-$SC8*E2bku0m54GIW2-r821i9I4#;g|=f>qSD7-M$B|z ze@0J(ZzVpnfZ&(^9?BBah947|eZ_5wRaUZXpUoK+z^w%8sn!qY9-I(C$cc?_YQlD~ z6`0R!=b@G$`c>Sq)xYf7G;A-F{wXUmJU;&Y^e{{QGq0t4962mTkPETKWvu$i1}9fN zF3*twlCY}rlbaA_hJ~bMRKn%f?c}@(h@rS=#@Qh)k)W9JTLz@snVx!*>V{8iPd-8P zkh9vvW-##B0ur?EX_+PpT)i!)bc*j>5wS6z|0YEg;WS^WK_ODj_rP5Tt3938JzKo7 zQZ{RUHKu(l%1lnq`C~PGl&kmfTlR$6UEtLO)4#@3Lb!?23kEiPdjjNr>)PuRcX`&j zvPBUKzk6FGb8?<^0AtU>=xmwm{rh#kXwk<#|Mq+X73-#Qp8HkLqgj$S9%V5zXY;&` z{9cfaQZ@c^%{TwcOpLYtV-qTUFpfxYn1S|?9%vAK`rv``P3~J7ZU%htw6j?*u_b&J zg=I;wG!jlDi+M^ulKpJ)`Oh!uV;p~0tQFg^u|ND}zn3hZXC|GTtK((!>6i!ALmgMh zt@13>1@a((yX2Q@HZ3{GS-n}f2vj|t+OEQI%shc4&n825qf4kXuQL`zUcNQScW7(- zLc5N?#GdXr=XB~{1Vf4zx|X8y2tA)rN{qWb5mTjS7wTi`juI|oY6>X*QVuvg*zQ4u zDK2*ZAcCmLnvw3fpNypM*?j}t9oZtD(Iji5aD%Yv8I_iCVJwu9ZPOlM@_|j*pc8HP1&*?vJt%#i`ycvm3qwDY6kY=+|)3-lL5{HwQzjlfH(+;*bdIvvvNG- zU)@~#%4cF$Ht}dO!fP?h4v9kZX0^mQclf<6Qx`*+VkAD_b4%&v+tg%O{^-~YqXSAaGBegBgpE!`kpBSa?Mp>&VY8;F#2Bi$t;(kVzYV070g zX{pgI-5^o||9!r{|Fb=N_AJgl?|bfvd*A20&YFgI{QIdfLG^@{61aIFjUaxwD-E2M zxzya>3@WDJAOs2VQHq!q&I?kt6;c7V_)NJk5@vyf=UZ}QNL9}=i=aBuY{}`t?2*dP zcko8k%;m0M5eO_**Ubsk%_12opKo%`i)#0Z->3_*@xR0x?Zf)|ZS+I;rj08_M88Vk zw{16aGbF0F(UKzNUGyhtI$OO96*r884t*xr_FdNwD!*Ix)lE-8F}$0L!z!l$QCr6d z+DeBT5%XH5Ce-^TXKs?g2Liu&T55A?qJW?$w*U@0Q0TM!<0GgBwhtM;Epc3aa~{0)^9D>y zWkT?mMK=0PF+i~|!2GC%LqRDU`8-7F5l-ZgfKZp54$k>(w;a&$1q(LkMZ7HOvsm4I z$AgM1iu6e)U-OUKrE^Sj2-z9C8!2&GDy45=2xqh*%EpI|8oxT(p=UWhgSKY3qk->s z^a~>K&DWPg5g1=F5>$!tnZMZEC4?NkejT9{?JKg)@K3bt4|hJdSB(#*EC%FNwA81- zv%zBp%!|Rz+XKFGEWk+g3wyx#+W664j<1b%^+?&pTP(ol&c^g`MakkFcA#c`-lI2R zx8F~^wUHMZvV=Ex#h-`(FGg@|ii+Vc!eV0h{hg~ruGAzixfRz8GEbDBtk;le5N>I> z4VrEFTOw?kPuCBs&>ALXNIp-{9x;R_+%)sOaAo8Rbqb`9nUMY>4wq>l#btYE*PadV4`N%mEXEW{LqE55@92x`*s9QL;5=-OA_@&8K6t7L9!~=V}qUP0<__#zk9KypI3AjuS@Xh?U23II2M4MGp7ns!7#b;R!VcKu}*+M;)q&Vgx$2}sY3pX*C(r% z7hX_xO>kb6 zUnFA9^%`2?6i`|u?|vC!85k!FTOwE@G_>`eJoPGvC#!BX=ZqOZG=}4X;1m)G4vv%O zcQuWF|Lc69pbVk7Kcq?dW9qTjwOaoUR+Wy%wo3F6u`ssu%GjRObC>ainUX0%DoFt^ zTVRlX1*`c_nSEQ!dZXT80`+FpBN^oQ+4#z+-osZs_@^%?i65@YV%?4_P$~saI}8#) z6}TrXTYlcY4(fnv-9A0yRytJn)ut9YvMMm|?b%{iX3ulKBL#RzqJ>?r8{;uyGJDp1sGm^E~_9m*YhDm+jZK!eJIdatxkU}**s(?SYAapfcHEvOd-c~LA zn3&EU;A98%-k|MDiX(jE+YF(cvzug6|`+NkX{xlvH?87hR8+`NpwV~6F3y1&Ina$?D2w_|9&)@4ld0Jfwev&blGg?%ElQ%GFnMDr_dhVa;nyP5zJj8a)CvzHZt^gF(;SmMs(G9=IQ>{`OHcgK>p$H)Wl;JvDSXiU} zrgCDJZR4mxp*hyM8miOgE}E?LsnWo(&-F)idFlq5|4F%Vh9<)D*o6<~#tzHbgc2)x zho>}c&NA+MLHmm{{O4-Il{CCz(bhm%UIeOu+bB|$&Caf+IN9zm1c^FGE3GQ-Jgg4) zI5owzHu&oKnJ>D5rDE#D)`fD#0qn#9YeU$#Q$KHwVDWwn*1&hlJ$h6H3o1%FTxG$* z63#w2GK6a2NOqW+Y$pc|_0Vo|qQ~AR=GF%B&tP)JMEx5nP0uVf^btR7l zNzTAcr|F=m$CGkpBI##`M*2QZqGH2pt%0F)*bo?rlgXgHP) zUF&ZS8+zV6g?ea$2OWRY`9~bDwoHoZy8yuw;wKB3zmi8v`z5{?TazJ=jxk+-eun$c zs=l(^Kl*Md^NR~xnT_M=2?{b2!y>H{#{TF>?TH#rb2NK!OG6uz9RYA z;$ldT0uinI`mO19#uJ>=(eCnvV;$b^7h_iBDQNQz3BUBCOc0Aa7iNDzV8K?;(Ti4P>HB2!U@5#=n6I=Mo0* z;WRl?H!a+h( zp4Pe4e&w$%NgV9QTYi1JAO&YJH0^H8VFprNJ4}nkqJ8?dj?sX)%xKu6m*OYmHb05} zkk)nya<2+&YrbS5LMh0AmO9wxzArVYmWjP6p)TkHOnF=kpki^`FJFbP#RFLQ2ZAZhzI}OLWF0bS!4L5zcucNU?&iIJkZJslY%nf zgO{XG91?8tI)K~a8QEZQCH%yKD3%69*Z|j}(2fi2*ASF7*cw4~gUg1S-^SPqs)HlX zXK_b5xS_|xwc*x4lr1{PJtT+cGXJ-pQS&15cBKa%ZcQx%gywwtvM2^X>CZU|ZK=m7 z75*P-Z*~9yko(>zS&u-BU0M8qC-6r#>Ti+k06>!N)Ud~%quG~^sJysmy(l6Ud2o0k zA=EphHrEelB`^wv_5VNR;;amj9Pwm`1M3kg)`={$!M>c}l1QFE;MGgr);Vzb zTvx#N{Ji%t8JfbUY?cToU9`ZPP*Njrs2IV7-k8vTqem9T-}9d>)A{jKh|K6a1yF1- z>c>UB&i@e3s^FPz+`y`xf=rW^HaK}EIJs7 z2?GwMz#BDp?^VF{sbA$Fk`ez2xRcFMdWuUWY^f(z_fj#Rjp0NJ*Wpjx5C2N^gudd? zlHzNk((-rJnEtco<@T(i4N7rgI*OmTvF7#d_l;c#N~=1&j++n#9PSUWln*82S)cjM z-e5RdO)~{#Afd7;w5c-Vr)4`o(e|e!Qc6>HQO4l!^w`l&8umitdfa%rhGA(XnPi#$e`Nlii?>`lq zxG<8hnyQnr=bqx~*j=~uc*__dG>BdH{Du9M3nt~52QcjouLe#7viS1~ZfEisjb) zTZ8#UDmkL={Dx1>ubO*c>1K zgSvtdD}lCWNeIe(qcnGgdHj&*i+{rbxFOOo>c$kOM9`Khm9G3VRTx*r`wu5`xD^gN zgdQ5at7NHG`+>8=>BX5u2s~qjX;l|haEb^9H3wizj1(DQ9nc4UNWcoUm0)yO9}|gn zQGL&~_yX-PGb0UNYP&Qz5%iC@E4MfEBf?Dq-=XIRJo{b66od?9vgIU2PywaU%7$}v z?N`JogwN#{h--4bu=D~5|%Szz& z%+c|2EdCiFztBqP<+JPBw)ZmkwWBS^DOQA2OWUvG&MM%hpz}6(F%746Fcf#>Mvn}M zB^|sA!XuVh;vd~rLAj^!+5NW;z18`?z3O@@%Zt=lWb~D;-ygiLvd=!{AEr&?Sye_Z zscD|ZhS5zFqeS#LAHL1vPoUUthEQttP&PJ$&p*7wio$*C$f$IXiPytNMD#-JKhw+( zbB?&KZWn_6r$Tr89x3t=e=Ee@7pQ-57XbQ-)t@r*wFS)@Gfdz-1-16?4w!o3w8(D| z#NF-_!4@X!Qq{o&(-0KbW`Txafy*@sfwm(Q2Hbx}CBEwow8e+G;ARjaN|t)g;Qv<) z(2CFUAVz(nfXLi`44@tud3p&|YQ>+*3nK}#0Jl|`uX-+6W5b51S^KmQQkwr6`4Phb zoSR*;TjF0~;V2%3;@5s}SA=__c%Oy;O@8ts_%^$#M)qIKvR;-pm}cg6?63Do2JXgbDTSt<@D!IHO}<|@z5Yiu#hU2w60d=| zgD&ss!5~)+*Mn+&Ywg|`cfzdK&-7>QNY!hBwKRC>2dzA1QP{SF5?;r8s~t;7Lf_;z z)^xz%mNL@qLiJO2q7>Qdo+MpbkF+GAoOASDJ+0%ZB5oNQ#3K6@CBfGA{R9+vD`{2p zp%9?{B{Zo#fz(PzUn2#;t^r@h2(;HJmQYvb6K+mLdsM(QO zR@0#Kj$PtbtqwEDgzS(+3xe z*RieI=+}L$;R2xH<2n3ZC=p1=O_=&?PfYN#Na7QCsqvxy>(!&*6W&H-ZzZZ#!^O~J zN@(JxDGHzTodi}I!dwe7d&Cj3J*f0MIlX0uV^I6H1u+((tOuGcZdrU;nLyu!AoT#j zYl!tnJQhfitL z?DOGNNhX=35uVIqx{L|#>u;pxSCW}aAMR%vjR_!0U2OjCStX(k9?O3$bP&2WX;cC0 zy0v}YbrRQ&2)F>;(tv9O(-iRlB!mz@v=Kl4n&&0B;3>*KQzu-sf;BCsYMK2di%zd8 zCWD<%p^i6b@JCnL<&oJYN0DoIB=7y%Z27O>yNoFn8Dz8FU(1!4%08Ryyf0=1{j`6t z4|OdC3f_Nou>6vDb7cBATNA>7mbz=J!AHso@paQr*YiyDGmpW=UI2{klVS zu$K696(dZ}dwOBOy*x8g5_3d-I#?}#^2?@69pNL5_#t}r@*ho6$}$!g~#RM)yFd`M5OVcT8MS3oCG zO|w7cdJN5J!|u0Inl1LZlOx8L2Q(E*InA~q^E2m^*fTu}4y319&_&oPQSgd0?dQ3S zNFKAspuUS6V$Yb*MjLy<27dL%5Yu_2B~**{bRCks&`qnc-_$0 zMnU=Dy0$`@K#9h6;+ut7NM{L8`NETis?n}{ZvdqT{zALjcQUJXoS04lf^z@fkqBuY zL7*wIeyL0j+l{_UK&Ks~H<+f-?1=NkP!lZhQ#1NDD;dQk#s0A%Sy&6L;!V%{-P+~= z=Z_eL`*|DVfg*C~8tNqlpKLF4|vt@@2c%DGuA z)A-<~6`Q4FypdmdT~hW!#>e&TzxL%JDq?kg!6A1)O2Tg>F7E>|Mu;I_8D*9Hbiw=$ z{BOoIyv?#v2`}k}m~d(gwHh~L*EmEd9pCwkz~+}c(t~ZZR9tiKA*W}lq3+Kp@gpPL zFJ+xP+qS%M=NKuIwdN#$$$!?Da#8HwX`Zb!%fXbbtD>!SFgmeXB^Yd4u^?M5g^#BB zoxCY;b~UGYMRN{6lZTFJp&I+`qnH1556}g-m5JejB5PoHyl&Hc+gPA;#~qz4gf43x z7wnZ~N-hn^qBAOjT#Db6C}JVY0*!K_I~1YYcI@0bah6duD14U`te=m@R@jg#2*as?g=G({33p%`g`V6+szDFJarT|%1 zmo*qZ@imQ_UDTm)#xJ$OgEPIV(wMda!PXvMc6f*wffFiQpi z;=>=i16eLW$ zqWZa?Js4J#r;eYCQGGDZBTcnWQ4$~rL2-dX-Vi&B^2T&|eeeb9Jgz+>i7QuFboR)I zGQt{vQ9n-&(M@m42N3WB`@Kb(raVTVW$C2KJuX#6U%%>>DeMD{H;AL0dVKFN>nR@L z7iOO|j>-{@JVF7D>kBYS9Za|slO*fl?`W4GcA-4d#_*v9=EZsg`S_4O}9ruv%G{Z%V{Yv^wqfYQ__fhopDbV4v zrI%KvKI=mmKEp^$TN4vKYPn5737@@(d<6%c-NPBR4VHxOfma4%0M7&UgkdA$vT_x9 zx!B316qSOL+96Hnc>P?GluP>5a@;qBkr|1@A~91zUF?Ou2M10X0ad+IlRjx;|N3pJ4WLw&l31ykdde|_D8Q`QQDC?K|*rRF)HbTwgxVw`yF z5iaFqc}^7kKbAs(Ok-A+UzQX+myumdP!e#t&=l(rsSnYQ?rBC;VV%0=7Y@-%5V=EH zSWkk}C)2UNV}Xxx43r`4n`$ys+xXYYfAr(z2Yn2DGNRRz4u7$XGj3{`@$Zw7a8$W; zMWL==|F|jaIO->l_92x7s;es>Wj{5X(tI`6954{AJ;2CErDMHe{hjGtfT1>cn3UlD z`;3)E6=2(tB(A&s02 zoM+h06_{d*iEkAr_;;%>bEo5;mp)Fk#`@x*#tm#`bnUlPFSqo;f&@&Ay0@L}_pDl5 zbWWJ64=dUgOlt4EZy8OFpGTk0Q3q-jVF1ySr72BpXIq(2(>ychi1Sp@_YN(g++(j{ zdp4+;!b8T7D@eo=0 z*?mk^q&Du+fS(n1I@ zRNr@L9u0k*vh~Z3zIs6@4RE$qvF)<5z}?XmPSz#O~Ay0^F$tVwptFnZWPc< zf>Q0dG5KnS-4zz#xty~&%-sB!+22F^a1P}b-l*-n6{W`5g=fLm&cXuQaCXu|#t^0R zR6%i=p2{hqt66sk*{!);bnIhnTBnSsK{Q6WTtYgCaN8tIJ#~S()|v zM$gb(45q|LdBbM>+YuZEL<4*cC5r1VI6*>{tbjSCzds@yP>q%uKtXEY=+bqnrWjfM z)W1L-s*X7bk(%PKKs6%#u;Da7hw98Bk=mtT@be{~%T&Q_IOTt)Yz5_0$}ozscENtL zP~6ocZk^mjG?udU&*tRRv>S=exRWA;tZE{t8_WaY>%v5UWsBU}9hP#EZ+mjjq_z0I zgT2D$8Hilzl6t2l#m~6&w@SjXC_VFt)IzwknKRlC;4D8RNf&N)o>6#;wN>4QiJdm! zq+%$#MMg41(CrGU{0YHu@S?LkIaqby!!vFb3Qffhrof2{HqlKrC;fRRx4%t+*o8j_ z9Jtb_?gdS_7);#7PgM}%|Lj)CWEcIN_&D%j{yQ`pCmb<(Ee2rQ3HU1etiM#ZD!3@U z+@AqO99PVhEG7Z}i1~TlchZC;Ug$uY<4eof=w^?_6pB2LE@z0&C{%;(K1%Tm( za@CnV<)k$!Qa}2jEd|8@U-=&XsKyQ0ZDB79yi~#+%#=pZ$kJcf%UTHV^e?`C-cgqaJ>0Dw(32zyPXynJC-u6HUuoE{hXblrHuAnz*s6-`h>Tpk7Q-%r* zKDA)Y)@1yGG7Cyk?0NVVSzQaKQE6F^mMG`uZnCgU8A^S?Oiy`r)5ii&HEW9BEw3Q< zY{ClO8PVQvHo^fmX-0#?t5fJyECuD)=<}SL#M%%Llmdh46TG;Wv#Ot<4 zhqlz=s9wMcKu^IY`VbU*Lf__!0qTyediw;mEN! zu4kMawOl*!cK}pAsTZemCGqjWGIXrXMWi`%0u*IdtJB6)f9z%0Z7(NmdxiDgAj<4> znT!HC!+-QcbXIo$4C?8;A;SM-M{GvXPD(v{bYKwi$P4L#@?kF1hNI5WV_E(q?qGhr z=(KuSUMZ3$2GbbMYns$USJE2u3M%Cf!A@oPvb5EiS*h?Ib_}Z1LInh04b@u>qyFHo z@3>%96TqiXP_o)&F|)ujH+qd$rnH;&7k9r4CHm?A3mZbf4(`sj44|3XRi>;?Aq$4Z zy_N&?s9B_(T>_!JY_%4-Qq+Ip2P9?8!YHcY2xM@f?)crH$#_sw%I`1tSSLXPJ3VMc zX4Tx&j6h6SEAL^q>;I;CCj|Uys~XlTE<6U6%3?x;H7_W_dLcH-$)~mmdety*bl2BeUl_`S+o$;O zag%hE(yJ&X%X1a)#OLBwU#mmUt`By_Fqx=5{}$dCpGYK*KKbc*pkkvCgQjC<_U448 z<5w?vY+Jr;H!52rBN!#mUU=AsCAfg=d_N~jiFca+7h92lFHPt5E4Dcdks}74v@iU2 zFfF*k60VN26#omeLaZl&=5$~31DWCPV7Ng}w;5-2Z_98{-DpvvnGAd3b3iOnT*2eB z|DrHdwK{T4ae#MrQ_wHKZk zlml{~1)BtW)&Vc4D-H_pse#-2QzTN9AmRY<0G8hc@ZlHo%bDp!XjIIdSed)zjKY__Lu@JW;?^o}uNH)wUea1M{{ zsRJ9k*uxzNu6@w`rm>Nzl8sm*{L>O*QZ@YsnX;dsPdWdzyQ@rw?9Fr7Tp=_b<;M5l zRx@+4itdzuzf}IW4qTt_J#qAXN~1^V@pgNbe&RJ| zz_`esd~cd*^trYuTQBVuKl&jk&YB+C_oNb~GzpFQd2^|<*~TUx48repZw2>BCbyfd(hb)=W#M?(X(TETNKIe2#9lC_$37En$lX zc#AF{)hb$h#B2#U+s2S8T0QwV#D&bE=a7ei4zLPL)?6d^vn#dLc*` z3;xy;rjXpp@+^;KCpJA}oFSFQ>RHCfK34b$YUfcZu|Dh9VZTHy3b0vIDW#3FKDE4% zrPl`0q-!bDBlQLJz?5L2A8cUA^dJkfeg(h`EjN2)AgVFfDK))>Ss7Wt=YRcCBFOL2A-mf z((7ke+i zS=$Odz@oa60hkuyIz85&cZb*==A_nSQgSM8h_A5fH8IR$OW2t$em5dn&> zCd)kOEJvZsa@X?a+!qHO^vDKOBkz@2{=$ER6nw$^-`iQt_w+NasYe4ce|60sn8HrslBB1 z=Nh7;6tx4*f6o>r+2H!o<+d8%oJ3*5EUh$Y45=#annJik?C_cF3nnuqY6Vq0-Ih*Z zL2set_Y6$F=vRxLhW1sySBtci4+H_^Ay=n|`Vi82-wwRaS1p?c4hfY=>9gk^D;3au z=JYR4*nEHTU9AsZdVoV;)vm`I%3ms4^&9$a!+n@VLFUiB&V$DhP*<^Zw1xgJHD=`j z_GP154V%N2HO_fWUSKN3y`V?D(dW06PXY@M=|m2-T79~8zyq16?T)NQ9UF95e+%~U zZw?MJu$8fU$0dnZH>(HqxPfuse%NNxB*Cn0X0yqE#r042Ljkbb{1pD(Dria;{8IWP zMDd~nTdnJ8b7cKrU}86EHgF4N!-yQ#Rh!8htK@lml#a^=$8rATWl6PM^aS`zSw-Se z^@lI__2Gv+yMxcOG+($xum_wlS(ceIr1XdcTm^pzvsvh$p?{QYgGZ0#L~jRE zAiDD9@p8F-uc8j^WeD+6+q)EVB_amOdaa+UKiCmDCt8=#Z2ZBmtita<(T?f6TFiB_ z{(fQN2G~NVp;&g6&3v-n2F-4Sc(+yzIrmM~v4`+PVGA=)uoFVi)eq4!pD=iv3uR@0 zJf@o-O>rDdL9O@lU-{V}(W7G0qI0Wuk<~8#VZHJ>VI*oxAG;d}FzLdMXw+>kE_0(i zjT&&S<{$kz-^BVh3pKhW)(HyCjnN{zlRw4f2VrQtB!9yX!9Jfc#iRSTA+{!sD9V>| zSvwA7+>i~}GIlm1OVHLeJ*MBBNxSm}gLPi@qHRAjY_@h(V`3x&zf|W)&HeaZYa8z2 zQWshyL;3pTJ)f#mw0W}QynFJ(hS$G3*rh_gzN5 zr)eh}Rt~7D)NzEHmpn$lr+6SYz7F#Mi#L~&sgnd_=e;-Z*za;A91j1TlSL7PxtASK z4jsb+&688?5PFS?mMlhr!WDPzl;w(0cxy4uZCU#HqrOhk~wI zD2e2n0!%{AjkAJi-oHPd0i66s!+a77?Ui?{<(?mj8%m|Iomx~Ew@TT+10ggx0lUfn>2C8yR)+GGzResVOYOoUD?>o((shp2eFvW(W&# zut`(4NWe5JiC}4JMxA_7tt+(7ANUpI%Z8!;1yw`i?s&>wC)GP1o}Q3>>w|08ydw7b zKcUABE6^J1@#>O^DwONl(~&*Bqvz|J74ZOHNExihey-B`b&Z``^TsRDKh^1QFwWz! z+pmE9>s9ctgYE-AG|wm{c@jMa5Cd37^FctyJ8HIUHe~kdfdNzv4WO?&YmU%DYO61V z%+4~T!u;r?QdF+e%p7!_=&?f;hNUo%UY{1aC_O!;anhdk8Qav2cg)oX6weC2Yi&EW z9~=IGq|uIO$LuQ%%e=klnx^?mS`7769}UM=8+yEsq=+}Ru_f`e}%g%#`b{@(`8?C^ThSzX-Nw!*b2a2eX+mKDG z$cW^vyN6>^%G-?KcFZwcCugOn&QIszN?G-GV6r}8SSj3{0AJP}BLTHd&>-2vSdbr! zzbsz`gvyU`)zg55T{6RiD@~jJ2_vZQ3{fC3vKV&S2IyS67@6&JQq6-|MUO6C*>E4$ z`PKDdi?x?n52eG;1g?Npaoa++@a#ST#%VYQ{~SE@o#-IED%BclV_i00FC5I6uNFlw zr)U0E-#)XB=QqLE^eiH~q}d-_7{d$7+y?y2`l|zzKfb40T1TkJGS}m(19v;})jwGh@srx+<*L`4QL^UBZCa z7aHWvw*TZRLw_MK%EER(p6Pv3KQMWtR%@xhL>~#oZF%C$PixIG;x_5MQ1^arxCkOT zRR!1=_1Ibl0%yx5i#FbC0pcgWt)LPvEiE|!9=)OZXvTV)FE;vQMTx*44u7@E#!xD; z(n$;Fl4fO(8namO4SW!zd-o+6)31K>MJD=@igC9ktUTN9Ll*qG2p|>o;YU3kDD)!< z>dtHpQKcoey{F1z$Jbu2hNR|fzai`8mwQPiUu(x0k$q8uWR!WhuNVoWRSIOD# zo192)MRD@i$+vk*V@8D|V(fv;mRBzkoCKTS=_0O1ptg#yVP5_QMAXIM8i-A0KB#XQ zeF}mMdG5RJ;K!l1i0l9B>lOr6HWGq^bpF-S`^jb(N{j4M2S2L+48nLg!2S;PMHGY) z1b39&=`a%YhIUwk%LLxOs7Y7GG*8a%Ct5EGa@jKn)-qM~hBs6*dGzAB@j$V+YCfu7 zS9;yQ6CLkg{Z^i7e_giR#7(oBWoZ0Wp!&tZR}JsoGv?s`R(1~ z<$4|?Q_DtQ&>&o~BBFFNo&%)BJ{*2!g&J@WvqC}tZpx<;UAdzrjT10lpy_aryGIE+ zB%$***`&ahNpr?3SmFB)g3jyj%w z*Vd!UweGPi)h%VQx!s98yE@sw9UEG?ygEL<>j=0(Y+au{Y}{u>UfF-yJ>7u?(=4L0(bZa5%5LI*41X|4!z|Lt=O)|Hr|BQyFWK0=Vg$LUG5_Ti z*`?0*wUtd!-MGMOBEo4J?LW2yQ}_3+NI}2472~akCN+#)X|sR4o-itgR=VzTCS3jV zxc_tWaKAErd?g?@`f$AEM2mcygez9cZByZg27d{Ctm2}q$Gn=udrn%~Pd%osWuTM* z|7c){>Vkw;PEUwBhb6TuXWZpYU~@9eQCL~}Yd@slP#YSVoo0E`vMy;@dE)C6A|eapXyT+REB0vTViP^qTc+ zz#}NiIZv9w-YZYq(`$|}j&_cOS4IdXFa~c=ni26h3lAWixbwd1`9Q2DK5ooFw1Z~o zZ~QL09Q}L!gVl5Gm2p=wH5|`{jm~YV-!di;0JEk?Y}MlKLEkeM%C$56b)2HY2i%3z zK0DDKhId1TyYvD#QZSi$5i*Viofipn$xdeW7!fSUz^Bue9#IYFqZ9AEttSTN;B@cN z>cstUz}X8*+_GE{JD&K2eFnD2^L_;O_-w-&)Ooxilx=jFd|d&UZBV;NEDlR-P}PIq z8Vjq8>1K)>4~@G$lgXHU{J;kZXybpF?yqmONvg5)`|6{a4cs-&?MwIct-XC^1Cbi) zI-5be8?s?M>Al!_1W9<@ASez^iD{kUzQ}CV#H$KRMVMEA-q@FGk$jVmHQEU^Upn1XDRBLxOf&*-!d*$H$uoHBEg-XI2 zKC>cZ%r%3}F{TXbdZZ!P+QRQIyNIs^`UmhG<7Ly9s`Z)w>-$&gj%N=O53FNs8B!L7 zBGz#y=XeelJ$+X_lRqz9!czpgH(eUV4SHiK^LXexxt_oAVER6yFCHf>wkg~zUNLAa zf}?n^(}nbzgT_=HSZ8UweJU=9@aVFr#0DJZ56r#~n4`)2^M>qq)>mLvWa+-ROkV$y zWldAH)y#zKFN{=Y-4D)?;d`?wuTWK*nJMPt#a`xO4t*TB<}W+?`eXYoa7GC$lzr$J zi{+(twEYUE+CzZ^aAn>GA$}3m0*bO0t#!Q)58d^z8D$FPy_7tJ^9RPIKP=g!^Qy$u zdGq#dJm!-8S&UXxT9i93gD={4$I6$wvZ;3_SXpP;faTe;XX%7U-0Tb?+cjkN?z7|n z_5!IYi4Lz^4_u>wB){pb(fqO!i;(pZw@jYk4^NHr#@(tnF8uWdh>h!|owbl{3NQeH z5cAnnhq7RTNIzdQxuSObyCD&KAE!z2NnM*U@VA%C%ZahxK331&@#mW<_ZH4==x%vv z){a60FWtc_CTZ6}hvux76Lv};zb>fd73JXp)_$ujB}Fg7RdJC}=9L?ibj9dO`u_#8 zgPOyX>ATmLpt!c!lO`5-|4^98P4!G!a+BM}ID zejxTc=@%j1R`t?5d(vMvBxQd^bKqo%1A^xrV>?4aT~9qHhQRMh+3EiF6Baxo<4E}- zD1FiAgN$ABe#2pld_hYVG6ru{)wL>*{v|hq5l;hR*T)0)EL)Nd_w>ty^*)`|JmYXO z@9jBk<P|EDlb^DNwIAU zZ(hRu_!jbU7N75duiLo&uDav3wkz9ih)bDV(#PO{;7679prEGD6 z_;h$CdO3L23vmB75nj4F4J;pWbcCRu=)sTnU$%k2MbaX&Zf*K%1#i`jo1|pj=1lGnf^dBFMGr=4N(^O!$;_N_qWoapk3wy9U=unp(_viqREcY!UfpE) zp{B#3?ozL$>iOukt}3;nEN{Wai#V}pN_Y;OFWe55l4*i;F zOzdSj^_ZXam8y=p5JRO{+B#t3H|g2s|Ku}}l~o3|U7_ixp$}n2WX8@}{wfV20@!u? z1q9iLC14ucy9-$Dg^sEv8hsr)Gfs?%vlphea8ibhfNFsI7$2VFx z2d(D_Zf+s1VO(17_m@o0p??cHffGbNV$@h>&-pD)-W*8XQT*NaN15AfVdgjI#I1Cx zi2`B$F}BYPoGVBySj{wh0lBuk20%UnV`8}tGIvQ&`AMz0A>^vEy#VDo0ej!XBlqvt zHT;t;ms|F?;q^}~n%(8>25LB|?farczrO(L7J&7%CC3m!^GZwy$+36dt%4#QjLf4i z<6uFskdvXGa0>9p&mJ=Ox!S$2!aM^HaIF*nTj;RU-xSy3;bZ{skcqTjQhQJEz&l^n zqJ_y2Ykc^N1}r7W?F0|e5P(TR!-sk&abpwG@B)min4u+a@&jhf9-9yR-GQ4nh!rVF z)q8s?GQ{wEW{XkV$C9n}$?lx8d&eFcd!c`i8g|;Hewh5^7A3T*_}-p8ZdOkaIpt z5Rb5{mqI6EfLvM>G7&S}A033BknB$b&Wzu9mn2}{KTqlwy8yNRlXr-uDsRH+@MJtt{BC7KDro-aV zwl@MCR;qi{8Y*msyGbLBn@4f1z*+@aWUaiHg2p{=K9SvRs)893e9;?G5J1KH|1M42Jwl=Wz66CJPsUA@` z9hP{hNe{3)!)`WUrIHNlHC1c?>%LgN1`wRYYca7}N~^d43(E8)lQC)lnjhfJVF^4U zpK|<1Xjlu|mTa{oH#^bOWs?LhM^Oi1HLZ4O$%*!}Fdk+Lc)qPo68KthQj6hv=w!Q3N1A(Z8-i@O`#dI=ZX13BL z;D=SD(?#X_4G(g;i?L(VJW5r%VvB3nUmldTr)+tHtml#FZ$K zyGJ2NcE!ozg?*pt%L7GCgZJHZ>4)1z&c)GdbJ`sDXyFqM4$L8gc#GZU`vAjC26fzD zUDrHJBXz)%FBB=L=*{{jz>i)BBO1-D+1QXQ^M(}Ojo8$K9B~giKZd?_e>HApY`6lH+JXIkQAWZIgF-6`w};UhPLWq-L-xOX$93Ll&jXJ;3%*fu5=srx-^X-^1ewCu zZogDxWm0JLlj5^UA}htg7yFXN?hGXzq8WTTbmW!mkYFrDZDgK!Xr{>p<7R|`!U9~48 z&ZSy1iOBjSDISKCY0}YN#X#RdXoDcfq9AEXlIF)g@3G5r$7&`W)f4S}7r`i2%uqgL z{Uj_VOdP_!>@^wVUCUAyM>jMc6KD0lov=PUYpLcxL@a(rP_0S*@ERd;-CulQWS?Z^t{qi z6!qKd>o|)e47ae+X~&N+MkCK$qR(gG$DT=}x+Gz+m;Ht|as(%UqWDfXetj*i=%XDA zS&XH$1I*;Lz6Dc#F!z9@H&YVj$ZIl}L#0f@Aw z`-ZVflgv0`K*dkWqb#Jj|6v{KY=*Ic_|v@}Y^|XOkp4;+KOH~LIE7hK3`raMcUaVeTQ*M}RSojIW0!LW2 zSWcUfjswd{*2Ej8H%$Y3BVSY%TFB(|b{slLlOB>+yZYsUi)<3qFsS>MOpr`i$&HEE z{%>PudA*ta;PPZ5ud^%T25G-E4LA?_&Q-zqUq=sU-Qq4vcV#M;+gs}rwN-J1~nklauimuv7)@1f$*y_zdp*PlG* zHE}Dn#oFElfT&GtRy}sPM_!?CUs>#I`|_~SOTkn3Oa!|H z6^zcC@EE^%X`ery*H$oW%xu(dq>!7|vC{pu)1b2_x4P@4L!M(%cVB+sRL_)(=Uq>U zNlRZ^-`ovPLgf4S_hvCXG0%AWy$~`v?DOpQ2k{44Bkx2C9AslR*U62K7PF~}Ej-$v z)RTNQX-Qhnv(1yn%WSY`a=op(@y3ka*wxy*Uk-f1w-mfzj7Tg?{OB0cP3o0EIKF&# z`o*-loUEKRM*+u13RBAEly2FXywT^E+77hYh=d#B+V!&a%~DI{O858GmZFzkXWuVL zjW$cP`aE?R@5}5hw=I|L?=4Rb(5yIKVH%LU`D9aei+^i`WSHbS#-F6j!Nik^Ku7Sv z>Ei@*(q_Q}n5>whS@jsjSjIW6V;&@BU#ddMrX0af_|s*!bmq z4|lWg?^!!oKZ*0(mELrXJio7*Nh2(o(N}cI|AIvi{#EVV$+^0fHdsSYOVD@_ZKz`C z`5uUW~=km*IswL?0j3^a1&n41nt>4+3a?F zWOn$VPSqoxC+wPpH(_7S&INxi9_#*0v3hWmX;VNEV-bTh9~BVXzht{&8*BUBX3f^A zev)}LtWJg0D)N=mI6}z^O$&AU3&ZkLllkgY(oQ$U_Lkka^T(!)a>sfV^d9?JI z>#|*nSKn}{;`tzl)PvDY(Go{T-{IU%FF#z@?z}q_XMTUn)$C2_&_cnm#<01$MUlng z^}g#-*EyLVFwZj=3X7TM`{}k@Eq&?L!10V}7{?`u2wmvP6LjyyetY|^^8?y!F#myj zHKzch;YsEL`-KH2H-*2KBydG=o&U6PR{oi1!0k=5b$^0=d#i~dT7YSvxsWUWQGwN$ z!4<>fw2ca}TR7vy+(Q54T0Hwi?gY=ojN@#-3(nwKJ}%#PC_iFvVtnE)iH+QAjl%*q zPBt<{bVWys-0!L0lW;8Pzz0mkQR!Cq)>YQw9fP~iITLbBb2i!{G3*605uW!Z*Gn}@ zCtgo~?A?3z+*upn9N+on(vHNOqAAt3+pUD}%XcRUc=MMP3&?sbb!$p%E5%;gtY_FO zu_rb{^k{o!54yj~Y)kX>Vt<(R6)yQec7Fa+*C*dvuE+uoJNE32_w(tFi}`C`$TKVz zt1KR7F#i}6Cs$%dsGwZLZ3Z<7SS%7b>f0oh1pgfp=oi7 zV`{I2tn=~S@A%H1V+BL?LR%&yP87Z6?jgP-UA<#oGSk)lzAg%j0VAZZ1It6CX!~XB z(8t%kI~nU^zNELL+cfP-J-cB=&@fF`9U05QWlc0)OjVd3_NrJryEze;@`gE<*`|!Q ztkqxGKYdMk&AB{j_2tOj@2#`QPtL{_j=qiyYw7)@0TuG`$2%5IFP&cWY>+AQB5b)3 z9(o+{8SNzWELU;fQW*D_oef>goO1gz(l>a`+{L`EqAS30lWA@$l;7}i^t^THCtb)5 zK}l{>OcCMEsw6AqfS59!U1UT1Luf=9T(@9K_NW=$wR!9N~ zBs~0-D+$EY<7_rMOtz}kMv#g1zVI>5vroNFLk1E^R`~TZgNvRSm@jix%foOy&JJ0e z^cTNI9Mh-9NRKC*Zuw<}H>{(_=VlIs8w`+dW^IvCKjLUgiP3TcZ1PT7fP%U@_|(66)yBrj?UJ)Q&C!E^jYBA1)Xsz5G4T)&5_P@9 zvta(V%LYd7Mp~NE7o8o2Ev=ocY=pfXU5N8QWWA-qsiTd%CBoa$!O2b9TaIh{32AUn z{91$yvHghq6*(>=Efhl8`Kk@#q_C*4D3?4H0)dddYHcg6r*e8{IJlDIy5#QeA}u1~ z<>e*pB`)lI)lTG?l$4Z+sF;YDm=JhE$nBbwyQQ~~lNSY)A%g#;+ z;=Gnt&K~Y^TwKJ3e*OK}r;YdJzgBW`+Yt*0C_=m=a!goM>@K5wh5C>nT^O!nwx*m|(n&bi&~e5`Aq@Oi4DDqia7#eEO<95%q`D6ka?-#Vw8 zuby^F5vxc>rlf|0J3c^n!Ue?`&Wbg8mtSp?YAPOCkXdeh zaxGw?$E|#1QSO~gXh~j4iSOpp?fyd+S;o#fKXHaPCm7nt==y(vQhi#TD z#L2hEV8_{ijlIc0m!Uf$y65Lu404g?=R$B5N_b0#2TSmejUkXooDapXZ9IU%>|3Pw zDDP|=fk2g!{#*g73x%GrXAeH~a}#tKeLq)#V2_Ys4;0W;{aOZYF!OT-3^Epjj4%$d z{Iv}1sn^dHkccx#-02rpqQ91bw#xim;g6U=EC0i+X`z5pa2tuk_g=~F=bV14Rnj{b zGt;*CWVG!th4ZW=?oz?nS5v>oqS&t$_k$+1Kc{nv)_#|N9Q_L2qq93u<|W)o?Y> z-yjBopZY1*aD$+MkYJ@DS33X< zubjy~x(p$whI%m5C^KEghVZd;=n32XkWPMb~UjekrC%_D82y~E7!wSf$~V8P!C8OgZX|?k(StnL7@fJFYez5Gi`v=ex6~2KW6yP znBhLwC!mtCez7&VSXg!v5A}$TbLKR~K0<1(sCInvXOxv9f$GDQ=F`KdTg+vb2-6Bp zathU+0t&wUhSObj9#70vaN7I7FQ}eE@Sr#etqLW1O{Lxj9ddjmPyxo37WSD@?cSB8 z>QsJV6nZuy)ng>Z*U~lKS>C<>i}>VT1d6-O;wCwa0qQZ+pAj3~VfN@3&)2AWjkh-d5vb_9R2`$PADgOv7`DsX+;eW4Z(_z_@B63_$z~ z*BBAw{}2L2eH?3CDalL6k3cCiUlatP84iM+ECDHGagY(dpDrUUYl+-R>KAIADjeh zjHCQr?MRCtp!bh|w)^kWF7g6ylTgfjTDE2h{ngR1)P}&!?pl4Z){J#dKOm^`6C+)Q zs`E%nJOsN)^_FbhC$m=WK9YT8I6X22gOu};S$+$HKQ{y(GjnbmHI4y$f)o#USEV(; zv->W6Aa`H$H}#hTd04LG`(fZ#Ap6h!03|D!9(1utL9RL3_vFx)24~*Ez;Gt-nUL&h zr{V?Dhv+_9hD&F`erd zawJlwo|8T!By-A42&{gu$ZiSnwsT^Q?L?%K5G0~iDGP{f04+ombfRy5Obm;`2?ioA znS}!e2tb60cSSeH^t1p8o%Nt3B9O>wInRX8QCf5yFMHVZ0iy0vmsJKw)c>6Q7$mhdQ0-S z1Y-Ix)DM*D2Yxp%E0MkC6W9lHafSvUJ;4IZ(9Y8RGKwgLed$4|U4bX^@MUTZr zaAR@ua}3xc*hK~DZmq#V&f*0ccptQ2hQu{aOpH$esV6!g^8p_NUWBIDt$sQnj3yIZ zMhxXh84$L;y|o?Kr9DqW2ka5-Vv}Z@N&K`U&|$nF0>#JJ=nI@RJNsF-DiW7v7nefZ zBlN^N`!-Se=@7&`c^-+==1p1#!kU>In%~ACO`C+}h=mW>h0Hb&`srE!TZKE2kDGWc zpVCIJS$nNbCoU@xs>HyYtX^J@I)IiHi8B&u8AAtcO9*GcBN>nJhpo#=B8L$u=IdVF z!(cHt0dY~V>KTwTKU=TgKsIy)9-Rjnb~92_4Ok-(cwyifq3?27JY8w`Nj?NB(d0fB zFgFyq5zRX!0<*8-(7Xfmzu;x91eA6VWPJ_;`?ML#mKy`Tg;IcW_SEemvQIrE?%r`$ z^dXe^lEp1Q5^O>RHzNZAIe>#g7jb&j{yI2e*i2yq2$JD*wiaNjSdiTxyDv6nBkBeW4zQyo(&Q0gN{#Ni`?Scx6c`kTOd0Lu+qy&SVu2Cp?^g z)gYe^XlwE|b=}SqpHw-g8dK!hhOB^%Pm)a+kEBe0eIJt;B3})bes$M@<}gY^&W{TP zuFli|F2M{Wr9N?WOAzJewa{h#n@?dc0beLf*%*Y4%byzNgxL?9Y2F1&TJW^uf`$Ui zy9zibspHB&9UfQNF8E%9@4Tf>bJGD61m2tDeF$%nc&)x7fKA9D6VC@eZqgCWN+}5R z8Q1?>krxQ6Ydk9h^-%OHass*MWPxxMgOMf@Cwy9AvOw31o_lq&UKkFW2lE_DjhbSrdy&r1U-uFl6*J^EJp{xn7a){dkA%f4Xr$&M1u86 z3sxYyi6b$*6n65-pAh@+%xSLJwGjom*0S}_Tf7P#E!0W)jg=M+hi@Zfr>Qrp-$-%x?RkfG`wL*vzQy zrj%Fif+TtON`%i5tnqW@f6Sf3WaIs*t5-6jbKwrE1A*Ba37MQuBUV{TH~=S0xJmXM zR*MZn_CCBe_jts7kCcJ zhS0@cz%6@u%jwhrA9&w=j19o06Ugz*W>623A@RRTJETPjfN@ZHkcBtxu2R^6uYuZy zSyX;^o#P1yH28zariCw|V#}`ftTusL2R$68<2%sUysn`qGq~PC1f(#NVroW&s1ZUK zMPV|7PeAro4~Gb&fQ1J>o8E_DbBXk@;5SeL`>O*!ui4=X+Bb_HYX<=(v5RxsAui;9 zr#Z0z?eFI-L$F;F6W^u4UK@pbcWBGrzmuFJz<O>kAr7EuD?)R`6Rc;EuX~2ZFVPrf&777xk}{*`4k=gA2~q{k^mq&;-;+ zb2e_}3fVZ1Q*ZJz(2bmZKrjTW4$ScRWYrAU@66kU7l1YIipF{YbdjNGy3rR{fa8RM zGN$q{u<_-@K`;8_XL`d zJk~RY5-qrA{XMJ2n<3Fu-y z_3Q*K$W^p?{DBM|LI{*sGy60Jnf*%L@9EePY(3)Sv<3D+Q8a;qA!FHhMI0&s3hd~) zwRe`Kg^}G%T^90Gc*FA)w2pGKBUtNKSnrHp1OG^*i z%_3Zn!9i-#k>99aN;6;YcqCB#0kk=UE%fB6TVl21xlKVS)tp>UY%7$DpCHPaMg_Jn%mqOf?(-2Gh*#vEm^;g7I! zXpPrI@n8sP2Kx5#!J;V5?Po5DuP6hUFV3B_}f-E@FkXMJ+%vBJvNPuZ3MP+HJv zqvqPNIZ_GNN1#YYf3FpB7O9x$1Z9IXzETxEm^<~s+-G>ST~q-}0jhqoJ4n;Ym0yi) zK9CdmzPTIa0G21Hy4$Os$AG6}u1X66uoJ$=y_@Q_se=xcx)eJTU^vVl=y!YQ#3KOz zkCJGp0Z|J32A2DaHhSoRw4TljIomr`x`f9akW$R-_HCoRgW4uS`fg<2guSN^!ztb%zp%W{$n{jsJbTl z*Au4}(Z1X@)d-mOq1k7tD=SNxiCcugN&@&TNWaKfa)l~xI%we{GhY-#26}14ynAyl zB-=cxg1U)mWil_dD`2D3JkejhC~W)ZA>PZPKWaH|5rFq*{N?nL5r82_Jk;j^O=0=c z*7KX%SRW?OitiW@n-~r8c}oBxmgi2`q5z_VBtFl-q&W;Sb!4fRY4`SNNJ8~Z?W6RT4uU@vE1fLh_gS2b96swAx>|QY4eGy z4VXPz3j|F>Sft48XAO}JG>&77-|5#c{6tR;Y;97G$9{N40iFT9EAWk zs{0Sz^Fvq7nXWUZd%Am`XJ+EGG?Z{KDKP;60M2`51swnY83q6#{6+5T7ZjOzO zjf;z`udh!`Ow7*Co|~InTwIKbit_gMwzIQKOG^s~2q-Nr4G9Su9v<%Q?sj)~@95~T zu&{7+bWBM}fkL6-;o&hcG1}VN`uh5YhK5#FR$^je^78Vks;VX?CeF^zv~*ZVNC*M~ z0&_?GJ+u2hg`<=fRqw(&=C{s`Gx}p|rmF_lWk==mI+nP78c}r0E+-}qYiq~ezV%!5 zgB{dvBqq*VT6Xmq|7_yJ?~uu(wqsFNLh7 z0p@@qQn)zNE>An09XLqqfG?7#I!+j1xt8X-6y6_#uYoIDVv%sPx3fI z8}NT1Gc3m_d{BbQ#i#uqZ08W-#yB8qcK>99+KB`lW7k+G<99v*<)pSmn1%pAR)&`-6WQX*@whlBpt3{eXV5esz?tnI znp&#Ws~hjG!SC(H7;j?H=4^DtP1PcQadxu(}p4171?4 z4@8$83e-)x*mYo)@}v`(4-O`np*48QGAo#qXz?eqYi(hMrl1eIjcEAG-pPVnHe8g? z5InPQ0l@h-yJK)IeeE9@F%JIOIQC?trA*Jm_=&+HBDGoeHg7c!Jr|9=<-DPQHFx9M zIdLNkuam$HrCIG{56T6}#itZ*{P%bQ4bZhLNblr7ehLooXwvDs?7v-!y;Sd7H3B&yVX{E;0lcqLSsJvox=suh*ZSUu zqXgH9Wf}TF2vF5|Xn>tEMyJ6Y0se7+K2`YPnO3AV@P=YIu{rbB)?Ix@5ELX-Q7cL1 z=GXp_Q#xhcWcd?dQYuru_`i1aJ&H-%;xj=!#6H8CEmJ^Td)60Oumk zI$?|8BF7Isa<}Fzz+yHG%H?376$$_P53Jlgm`cSnNg$Yk)@$nFYrjN94^x;=)Kw!) z2kiL3Yy_eyvJmcNNBq%tHI$!ANE-fI`~wK~FPRoAfj_t^GqQB%Pi;s72+!uI*cpP!^vL1slkOeXK@I&a3~urcdh($jKm$TJQ86N&@JDBe$3X$6*Grzod&++;3VZ7p06JwY=yq<`ZyoS7jPsh}fqKMM!V>-;T z)M&`Iur`4(5Y)>~Ofv><0&}5OL^Y_{BRPY9CX@-8VR7QvJ>p><@a5O0!_+Llo{u7x zt#+~w*=+EDCCQI;8_5hl?o8v6yc$SQOlVM>P&ohSYXhMo~X^ESKJsQPgC>zEcevT#9P&qhcoro`(`b(0jgbGK`XvD#&PYnXb>G zUu(m4Zj|P3f`24H&E*FNbk(c6S|; zhL}~cKdL0@QxjS0yXwLP{OH%K%!$qo7n5TJzO{;pv_V{R#j(`=?11FfRy!4%>tI

(`J&^85Vd{Mu>_)Y!=|>PTrY4FkJgya9Lc^@kw*OC$%FB(H?w1Jk-zn;cw#Ib(EgX$upocszni+e~d!P6Tbu#}wT19cW`0$j$ zOFd9&MO9uTq-iP=bvmXA>R1az23^A>v=LznXuYTBf)L37V~`-5*Zk)~i6BzWZBh*a zbUc9&9e%-yDL=@(2mJhoYg%qr9Q-H_j&hMti4xLHN0wcuE*rcK1a&LdO2hazW^j-G zsTHVTX<}hzD<=@2UPcl$jmJ^O>xmT50~=zt%%t0lK+eup58YCP5U!J6AaKSlf8jcy z;=d3(2Z7S?tfi|R+A=(DtPt&me@dU6^Kb)&CCJ8T5T3t9_M(T#Vk~#-bRwQx9=-gf zCB=nL;4vxl8)5$;jFJAN-K6}&0Pf~U{cEEF>K+2@(tG88BmE>Vi%5&`PJ2FVHf8fh z5{lA9L#?lw8_EE5H15raSs{XOaztyD8sx+w6gq)fc>?;?sC2G+>rMu$>zWYnW=PE>|8X|l`1`X{r5?DIbLC6b4hSs3WJIy?$uUCpetOAkhGoA2cSwU z|2B~yiQ4R)_Tn(Ua)L+G>`bt)veTCt3 zf_rsw!fFyD>lg%qrR%yDJO_yQH2548k8mxDTo1>jJ)=P=5gyf74 z$a-Syk6#spa+XkA*@>E|W=|34=>O&=x)JGGuWX^^_OYW`r#ZJi=A;P1T&jjg>Hd+P zk$s2IJiTa{bMW^uFH0tMrxvl9W$WjThfQ{%Bo;&YBDI_CsNF;d{n0Wk$*0nrHU87&j?h!XF>`$ zMH=xjK5l#~WFg_~*WQ7!PS>^+fp>n1O`lbFa{c~B#y_XJN>Z^;Q$i*Ad%L6|!I}Vr zNSD;z0_FFOdAs&e4~pfO7TRuv*>3jl6`^kqOBL?}+kSiP7L|RJ5G^wESd%6smAo_b z&_cvv0J#smeQrc09~>b_LFksA{P}C9^m^+7_MDDI)}FX(ECUQr)9SgKqw>Y>!2spc zxO1LEjg|% z{KPW}c>;emzJ6G5PMw=U&$%u$9OzuJMVRn{S*r>+dV~q?-8`Ro*)bRTsx%39EbN4m z^$8Rcn4XIl%d$#fqdaCREAREt5oa<#vvO3j&75*hyjeT_m-;5E=64fHyOuF0b%;^b z)zuYA)7t&LHXu|cDF;^-JBI7_MwyePvjAU_{V}>^NF+#!QEm%9Z{&C@sf{CyX7J_3 zs0FCG7IXfN>Dh@`kTRchE|6W~_gx@fCqw8}!0zRC*)Jv3g|?PfvFLY6X1{tEEuv0O zv~}2StG%_YPd)jFfWruM2EWaw@n}<2*<^n{J^g0Zut(aJh7{Nf5xR%1S(>wMAOnGu9YJjPS=Uke4h<} z6afP+>ZotNeasGvSJTK)Fw~$MItPm(;#I{s#9`eH7$J^(g-YjHe*TulY~TBnx`;+b zv4A$<^p?D}wKX|6Z7Jeua-^w!J+R)x`^>`&#Ujw_X?3%p9MM3SM?6r8POM+(k=ohS z5_@%bgtDy43s8H#_PA+DvU+4@H$K0xV6obNln*m6vurg0&UU| z7P;C@K`l|IRV)URl9qaA8Bp`Y(63r?Y4kN}M;$hMalCOJmp$};_`v$K%#!OHIJ>lL zTJvFK;or*9@q_)LPtEaY+Y{KZviwU9??R?FB4#!pTI#K%xz(1S60%t zx-Xx})jq#NJ#4E&Qk~#WvmsQ7fB(DeXY6>4s(`U@++soY%#5`AXIv0A#+CaEZ%3#F z(_yYkiFVgn)8Fs8m|ZKpW)Os?noU*<@bhI6D)9I82I~i@VS(@ayS|}MFchIH)bi&c z*I!9ptwuIZADX%yZqzG7>pl7As;`D+Ls6k=5I^2#4Kk>kuq}#&*f4%C(2@dh{3$qqMkB@FvdiTvLBLh{J6+KCKR&9p7lry93xwd zHGob6S#=z%ij$)32TC9;(VBV#6a1Z>?(KV_qkeSIRgpM~KoVZ9hCn$n83l*2vJnP5 z0%xB(^4i3hZE8IQX-$wy*7`{RGeKT6TX)^2I#*-YP6KVS$rL!L>}HYxQ8jVM69MXp zOuoH10*lqbD=t)!&fctOQe!|IHkP8gxT$vY!)q*D279cMbqFEPNTK=9no1tg_KYa` zj+;hR{Kf{&MRW`EIoP=m=8wVrosR6SU@L&izfQg&g>M`!=%Ai*(y$l-=&3{5cpfrq zr)uE}E49ZvIScJyityw=4nU4BIndH+jIV-(QI<2B>97p;?d}&5}QPl6+AAL54B@%S4AHu68 zjGD*q_!ZGq*2(DK9))D*mjBovHx;q4Aq5SHWz13DsuIE`YR3AbwYo^V&WCypYuXE5 zgi_xxX~RTBjeIfu%f`&ZGPWtEn3s=N7MIMn9&Z_m`l{_|wDvnV;U=L9-_GZOzK+QtNsNspk`D5e2@Kj;n97XdzYpDqq+STo8D zqs#Mg(F|BWY-`BKV$g=H%#mg$cKcu}J&c9A?VY(43wEqU$pj)+KD)m{u&;0G?3(?T zI**V>eUzDgyDqWDuKh=HP|(3h$gehY1c=C?DYXj?o(83b(8m$n1aXa)`uy8_WpXWC z(A@wJ-J1(0u+#0Pw$YWcGHUr_>+j1Vs43&>>Q}aJ4oxL1+8B)QNn&YA($7R0Ww^t7 z32@%q%DzFKWN&SdB=j^XvI>u3wkXYle-faVFD?P(6GtPd@D@$Z`RI~D7HKfvXUSxc z(vTv3N4}D4j^i5U&5CmlVYZH^y;AE$F+em}7li>Alr1S>=-YKO;3WL_iN+Ru$4M%` zKcIlFmB|y|Ia9!Xm-lPVSz&;#*%@}i-pFhcpKg2y1DP)yx1ifo~KoqQkVR}E(Ymp3!2q{~s z7!%;E)qE`PiL4wTgL=J)_TI?M4u>i+Dc|UMj>*csMZ=AyvYoG!85c`PbNLo}?)8Mp z#NZ{lVHx0@?vL5So^@$gdJ!4|50D8SB{|GeeqBCuT%e!#g<}_GON*Gs%1#%fn;~j= z^f42)M*{?qzyy^$n`MnTScDmQ2Nrjnk2O5CO>cGWyImZ$eZ3w-bZQtW3KlNpnrRik zoDggKx7}TY1PPs8A%D@R89( zTo7D_=y>uSl#SgSea_QT*bFy{$HWv>qwwFgfO4v_ zLxDY)%W#IOz~a3Y^5@?(-%)l{m<~9QweZ!EI)9iu!PaX7o!+mdvtn6%Njd+80?hwv z6ceSMK8emjM%LV*LOv3#8pd+yF@6C_fhD_1`5uqfyq)*7z;PA(u%zCJ4RUt7@f{am zSb_p7y*k=LB}Gcyotpf71J~k0y=!jV?r}kgJTkea;|93(H z7YpwR{cRIx;<_!~p!0Inl2*fKf0snk80BEv{pKhQ$Ux<6;s*m7Qx9Tw&e_%9^woCy za9E`pxieH9UQ;)#)lG#+&`&U{+qavO45!zI(z3E7ofDSoUVhTkGe`yBR1TYF5$ZbU zmf7&;Vb+u^e4BIt`125}`$;mhE%j=HgG&oZCU%|aML-Y3)q&JTzxmpqx+GJkU!0CTWPeUa< zR{PkYMvK$*T=Pa$`dFT$?ej=v`Awki9mCDa%6!?>0Ms!>0U(G2c?R`{;xW9LEGXmT z;k3Y@c>4kTw>+}u%^RT@vO&ay{`1mkX$GRA_yM%MG+D3v4}^A~RFp8)Qo&Vv*2Cadg^<_l2q{5<6e&O)8X=vlKl)wpv}})t6Y?u{m=6YG4=trEuq5 z)#`nnT;0R*3vv0cW{Rff*mP)l=DxpFhe68pnzz`89rIB;?O@Sg&uEY2*SG%EH!q8J z+;g?I-83`yE!eBHLLOJ!nH06lX)DxSbntxEU;f(B0tL4^`e!CC=9QGegz`%L<_qAb zELiIKIkP{z=Cr)FSwTTQZH|KsrBdQN8SiTrWNf@pv$rW?qa)PUhJY zgUSX36SiY|=!bKRP8gR@ma_3oU(~%0l0Zicp${UPaddpisiy8Tz-9_#m$oRAOG6mj zK(L}*+||r3kD7ApBVk~E%>CnuxF0A(eZ=kqyU44DIs`QujAK+5AHd@GU%n9& zpCAQn;(&Vw1?8&d@uLx4fM`r&p0ti1Q( zNoSS>Us02o3SVfR`@ypC>OobM-&^1mb$yu0!@?uF>VF2yQf}*h19wUs8NSX(c(`SBupj_~f4z#2P zGz99al!e#=)hE|UAIylyGCp&B$ji@9Sl1Uc;n4xr*l3efZylh42@0%+=FlO#_<+EI z8yRV>=w{|7GH|E>{TGQI zqc{C;X*-V1ImA7Cnyhq)vDks$M5%{eQH*M}4-s~;9oeB5(qn>PGpe;r*a#&-4jek2 z!>HV-IfnW7#fQ)2!@qSwfnP|nbn?#@+9GG=oDE<1D7>a^ObZguTaIBB0<%MUndj%S zAV)}m(fUpI?>G$nNs7y|-CfrllqRyGBD}f|s(#eVHO3$z{B7+LcW<7M0O?0dBXk4!BbH_IX^QF)(VW_zYJ`_!tQ~cg#IE zj`86d)jtd3GPB9_;Tn?z%{pn5?syrt6ZdX(z$_cJ_NDC>in#BoXb3y`K1#-jxdeic zo}a4hJO3hC8g}FlGr`#P0o3~#gKQRCY6Y4Wap3exj|A8|=jpE}4T;@J7)`~0mQI`5 zflPVVkuZs4d|m^G7Y$I5D-y_hn5Njy`8yV#H|lslu``59hVeMAs@B*V)b-RR3CtWr zXCq>G|Fw<)0oGMieY%96GiS(V60qRyX2|JNupmR42NQcf#!ZL|u1t(KhC6EUnlWbB z>3%$barc%slV&=doqKZ8^^RuX8)&CzbZ~(6S-EfEbsxyZ_z(bjZZh3IctQDF0iESm z6O7Q59vCzKZaW5fuCGuQklN_s$Px^vs7`Jl_Ke9q)#ePbm zV^Qa$gGZXVv7`P4$83GF=)Vcz-Z=|X4OY}Hpp2p+-BRfK9+0xq$*^4yF80f8Xk8!u zpqA}83gs*kd_RK9d-Es$2hwSmUnji-5#+^8(C+KK^z6UYkd3iY*c*6IGXu9zDSG=9 zU$W9)k|bj{41%5b!Sgm_xISgq@iPQJ+?nQYfIe~knL`jOkUGqwJEl`622B{CC0C8@ z&crH+sujatXEn|N&3>$<^^1b_Y0bV-Gu7E~0yk9YGkQfYX@F?PeLxSPdoZ)%Z~iUm zTl&uW`srIOA(^K-W|6Rp4Sn>=xV5cCBzk$;P`jLQHBZh_fg=r&ac3l6VqF8qXJv2| zz#{C~_}^_b_eBz+?Hl_W8iT_YON&EMU53J<&zUwweB}Y#eoEGV~zT672Ia2)gL=BkD>gj|g6oj{#n)-^4{wcJ~0q&Att7-jW#k zZY(|cK?hB&U?pBijHqV4h?rC;Y$_1sg#r!cxP~=eer;RzgJZU|+b~;6G2wgg`HGqk zx2zVS8MPiSVPGM?yQ4Na4qNPTB$|P^twbr>A>RukrB(foY{nu?Qco{~p&P%)Y71f# z<1Z76vZ4DtIyn29p_R`ej)Q9Bqu(Snx}D1T6{ zG8yuW)xn>7T5zl2ghv(wZE z8VX^G-Z27YG?l>ePohd|M7ApIsFj`#*blsN#V%~bS?+GaMA*pSuq8vSM>NMH6<9L$6p z{C>e`o=C%C;5FxK4`h4Hv0q zGV_+Cx=YrP42S#1{)YjUZHt6OI z5z8ny^|LvVK{8OMuz2I`o*2;V(TxPM&a6dr%kenI7wdd$%E($|D!3Qo0~vU1xe!n)ZwHiDyodjEuyGPv#MGO!{B$j_UR8duYy(`Sgn7 zL~RBAsGRsfRfLGJKf(4rBY2NgQy^!@E;mB$Kh~c7^PW|~qK<@}k;%y`MZ?~|q%dfQ z69x`B6K2b8+tmyup7j*?`5yXLl9W+>8kcK2uMl!2cjnPVX?D3wz`FcHiPe6Iy?z9P zMdtwubrZ9DZLk6VnTQqnL0!KRLWiNKf1T9-Ee+_VWe>p$p4##64oFvLDZfB3BRUZ+ zvLQjX_zZ;82069AAb+JdnRJ5&tNi6_BRYwria@^R#BmPn64J!7ZFvm18IL@sn+VJbx8`(z!@cbcDom}f*)EP=|>6pgc_5k|IZeu05+gASATH+? zRwfN>#Ipiyh+x$_>yV)7hJK+H#w4Hofhq&}fmOWR@1MOKpF}S!6+?x)BImy9@QVQo z#Hlg@J>5x-&{Ud&ihI3Vzu0QGJ3zJx!nUd>or~gfF5H!88=KdC`s%>ur1jokWFl-j z%d>XmQK0kQVj1;SnLYNHs@YW&KUV%1@pi^4Z&OdoF(>nzy_|Us8Akui-U}gF$I$E}IoSRrSmP+I7Asdue&I! z>#bY~yZ@4MMGHwm>&NH^7@H{8V?Pu!n2Ingff#KA8uLb`TX(JJawpjSS!^q)BF;=? z4@d%As5jcS00p$g_^z-$R=dC^dv+i4 zk%5wBRRn{wtvW$!6U45Oi^fCXSx@Iyvo zKP>tWs${MegKrcX zj*RsC6WK@47^kY^2$^F1nH-|HgWQuqGU-9@l=|%K+P(~)W?@OndOU(bFEK)RMcQ?r z$h{GW_MSfOjQ8Tnqm&v)9+0}jvD@tDQmSpjZjNyfEZXd4j7wuj8aDZgvsd=cVV#M} z+|~Tk9rzynnUf8`e8m`joN6AqSK{Eo1FTaQ&HIQMHi*l+E`UYr--)mA>a7z+uiEOf zK~j&70V;yQdSeC8ZSn*m1OX=^NKRePU*cc4eaU0mF>aSqjG}!l_%kS=FtA^;0Ks2T#m`vMfZ*_dhU9Z(0`kmm8|0vgNeJN$>^8 zZnjnSpc<@tdl|&V;|E8y`Dvo1W{vt$nlOUGPyG`j7u<6>0rK>2%&b&(!9)ej1)zt( z&r1y3gIS=9h!5^OK)HQM;Ap1BhFpHdn2Sg-J#WiOPxx{!&K|X}4g=La4?Nx-i;yhF zE%VzNloZlz=_fn6S?~?P9Q~k?dd5X`|Bp=yk6Y$B1`UmAOa`Rj9rLd#fkpAMHtM13oNLOJ<_@IDgwC}|W(uD$NgefJkam2b(WA|Ph7DEf_ z=-*0in^wjObwX=+q|J+N$hQH42NA92^M3Gf@-L$YW8bt%fnjgnd|k%k>}wgv`UH?i zK~@K2Fdi?)n`2b^i0;2->(c>gWd5B!6EZCm=TXfeIeJ|WB07F2>uXC5g?{!4PEhWI zd|-g?9CW}CX92xCa&(T-b^3ed*{Bw74gYMcbaTAJ^F`(;92JPaE?A)9ybELp9uxQ^ zF}XWX_5jHCQL93Ra1)bdwHx%sv;yCPKMk{%b-67-U@TAf0J}GNqbJTep@|vzupW^_NuwWFTnC1Ct1`th%pV z4e6dz0DgO7r-4MzlFHVe3dbpeR4}n<0z=<`4kW*#(uH7fQ+zA;oq;?ErZBn(za{@d zt=ow-tQ6mVz9H$Va`(+nt;HIru!c>M77^KruggXr#O+z)?b=DUN1F3p4wOW^*}dRt zT^tILkIC{V*|UT1@9v60|{KtKg;zPaWOtL)XIcj~^Feyl+G z@SgPi1Cg+zM?(q{s^K|af+>14Lwr0=DzejgNQ_^eX#FMs($kalfT|pqy|L-!*dI}W zv4BU@SwZ5rk<4Ph4utoQWI(>OhZ_SC@PxkctS!qgTd2gxV*5P~e^2~vpb=wpiqyo* zBA@yVz=a3ONrr-E)=XiYTNEE3mD3Pm-v%PXeKVsk8FJ*UY&C(5{xUMGr1KwjbDf90&m}bMUYDu&UCElt_5sthHTF$i%1gfi|ygwUaUX@ zZdpX49^Dm=&h!b@TUUx1b2G-foyuuS^it%^&ug`_5$RI;)GU{^3Z9{mVEnHl9GFP6 z470Au3@ZffhZ9%J6$IuuA^owk9vgjJJ@+;2UrbgJn2dzNXQf;ho6kaddn^y>IH4QJ zl56f?68=^15G*!(Y`mC7`&5d2+~`X(?)^*kmOB20|GVsIXsmJcRk#0BIv$;L5Pfsx z>X`hgv@>MYZ9`mauk^>KUzBps{pHz}*ehKc?44+-t=6ZR(v(Tmj|A8PR~XNP-wea4 zUZ$`)LtmU~m>Zt%mLBNzTQ>bpR(^%fK0Qsg`ElHTy$AJhO87-RKV#IKo!zX2zNmSi zjtGyuxmfLEL9wq*!B->3LfW?EpU=bys};i+?XnuD;NkZsu0{JC#J{3|8ckez$B6Rz zWIBV^#HXmw)2f3Dr;GjV*RC+K{(Wv+69D~|bz-tL!Fu?WH6byr81zYjKs(~`|GAMGWx&ecph~PH=tPtMO{_UC&hVRj#j=Wy$6uYrR=AUHU&q9vRy;nG zqbcWDx;>El%cH6};az6n)2FH%>8F2B7sGVyU(}=|jN=~>z!Y@S&(Beov6H-iXMpb* zA;ipMq*Nlx%SUmDVgm{rt|in6ph0S8+ec76Mt%_jk&>BVSNIRk zJCS(|2Ihlj!}6-mZeygrM17&J*`#TlNT7NWz=we}9C}NvaIZJ1SbbKeG1&>pecdMU zDg#52!==05FO1@m!#tSQe4&mUCn)LVI6%RK}bZ!4|@?cXdHCR|k;I$sKM+ELj zMU_TnYOnTJL>VQr9hQyi`6UP0sZ(&0Z-)UFDyZ~Hf>KQ6C_E1a`AKHL_H|LHezl!8 zY0Ea~eeG(k=n%Qzh#=P{zq)Qj$zZ(A4^1uKx2Jggu&m9!V%Urh`UMM^q}Yd3l?5tt z#EY@ruB@cmB51N)hFJ`8mZObqP=QCCuUL(X?iZ38z!i@_Dk3KcA&>-rRA%qLZNgC14l)J>Ycn>N&mf$VVW0#whezigdSB8ug@(q1ancr!{p)!qQ#qstD^W_1`l+F2@Aby$IMfu{S%G1WF3W z_bA2X{RMnXtFgY=`%LEdpR>v3>YFptL{1UPbGc6#slv$A@k(`CrRGOxh`l>NTymJi z^K9b4cgV&53MF$l};m`9}u2Xaj$_ZJfnGXedNKNqSLgl-)*KKy(#H7~Crntmu~T>`7$ z1nN(nn1J4~mh=1yy#oZ@nH~iTpYA0rA6=hn)BNqc{}w?^*Xly{9O zgrY_2qlX(slVon{#2O?%?AxmMY#&Fwm8T^#;^!l?REJsiZ$MnsiAN01%d%SvKS6zs zwYw@vf5YJIxP`pFi~SV6B;YUMW7yd43g&VH>-~Ap*)5{=Hbh*sS@?Wv@@SqY}S z{GQi6_Th2yRp{(3Szeu}j}!|A4IkOQR^e^dQKJuoU=>a-RnG97rHzmJ+*X*NHi+Nk z>5NwQk;WgF&#?82oTE6eb<{9Ptb!fP_KL=gMWlx^h|eT4@hGIByn-&NX>GZO8bTvQH{wo3~oRz!eX8E1Wi6QRa48~=tiojowQD1OLKysu7}+!{v~?OJYa!?@1HKv zHYQIB(9=_PcUGjs$5VcVeoIGOI!5CHp0|iw2JMCCl}S<3b$Ul1EK`;&Wfu&RfWd!m zZJjA)7!`UMP0$Zyezc`oQG;ZE^hVtlYg@d-J3_0vM%#qlev6eW5>idV~pt8FFqf(+TBxY(}WOw$q)-uqWR`os76!sOHNzy9>@&0S1Pvzco1gXK5XWZtfUU1xIL5P7NEkqpDwo^d^tBWG+h550PPH%SgIj|w_y#_p8PAP1dcPP7 zJ1lH|D16g8aVorVBdNLJe>^%S z?yS0(OEd)Dw?4+30eTon0^4wB*IT})*R)ZxbqWnOr zHH14KFokM0s!uArN(<~vrs%lnbO#3|9fpfbY!$FOdh+`{VJ$!YS(wDAzD*bwV%^T( z1tp}l>tVlY%=;J2eM8zDF9C}M=+O|i(&oBUVchmH^rc<{+M1|x)E>Hmp*rn7t7<3@ z_AyLD@UFWDWC*bfe5r|2lnT02E3}!WI`Z=CJlj+4$ATmB^)%aXvJ!mDtxe3uN=R^Q zI{g%}`7K`&H<|)c3w@Udn+Za)4u6lNd1!r~1+G0FQr}zO!gk`Gb&mVYh1nSHw;o63 z!x3Z%;!Dt~`?FivWrFEplEiMZd>P{?<@xmujWtO8oSeB|Y7T?5Y8#g}j!sdQtrrW< zZ_qAf!!FC{ztE`Uafx4xjxrpUCgIzUoB|_c>dHKm0rf>rm(w%YuS^&oFjl3*{*a{X zR>D0;wEvU|qcT5QT*7kzCG$DQ2H%MqsjaGysg$w&TIGfCcI#mZ`)4TYj_D3yYiDOy zPQbDccpm2fEu)gbEiH>1{Xt>dP)rV5^o(U7<9C2(RxqYtGGFniRRw4zegkT#0rQ?++3@trrBCq!JjMc9T1+wRnvaUJJ!BO8gXS zsjA>lw!`sp)TAq0kZ?wqoy#uut&!)?4{Xq08BpO?o$WW7j<*`(4c-DMxky;(W<(IU z{ipz6%InQ?X#YR387DRXNB8|m1kAwGnLQ{ML)9LP;<@x|-o8-?aMKFQh1^8ceG-(y zXz>P=ILCQ`_vjZyR30F3=Am_m>4`Oyv&j$ea5A?@l{N82innKeo|Pi^N!Y6TGS7E; z9A6e~jA)jIrW+t1%J^iI{aHl!rzju_C~2QL>u&M zP*Q`LFmsg#Bjpz-GoWJ@fP``Z1>zqM;5u5NW2941+rEuYKhS%lITYK*O-LZE{M)J@ zjZ*b9=!td7{hT!m2XpT3p8s_TRpEW=LI+Sd@lnpf{DeO8bFq~NsdSEENoy!@n>2HjPR}% z{PEJ3iH+Ed_=#(~)du9qplZhR<+1+NH--oOp{D7|$A~A@ufY>Q$RPIV{@e3iiC*bX z#j!}mDVDM{YPCc#IfUx2_?=28;uY~cvX%&ZPsjldi$!w7d!ny3PM&oON6uz{7 zu)Bdb?-&~2L?c5*h)Cz_!%~$s)(jdjbd|A0r31p!#AhRI&Z}ayjUT+b`7PDbVaIdi zqb88x;VMlGjJah1pI1CH!#;va*cTAR4tzhHxUT{$=bJEsyqyo)3_2e9pJ^G8sPXQ* ziO6sbpVsnopT~}b<5%t_SEpja^JOvM{j}n9)q_F=N8;l!-c1MWeoDnH1`dxI)h!4; z@t(p^JqZTLw3YrCD`1oIF8H~BEfxA(iRSSfe4DDJ7!ryJ_8SD z@lWpgkumMyU`{e4KmHf)si^`Q%@wM|*+Hr&=bQm@lCI^z{%pO6ZLkYl%mfKwo*O*Y1J;bNDIM=k?mLlL@*iTEIaV zDl|YKdm1H7IW!O~@&O5aCsa_y7!RHKu)mjA_g5VhsubOu4r|tP0;N_S;i)~lk4@Rq zL;dy_M3!7BdNm999I#)>bGMbh7WaY!bE5btgk z>Gp7gbwc(>iyi-VJe;z9THn~=K>~lVu9IvW$%31p64U?X?s2RJ%@4e#_*kbKo-ftx zP5PA}%)MR}z~q#C^t%VpI!CL?5awCm$F$cz1P$d~69g_B4xLdLoGt zooQ=%(@!)XB$dF0D1kz=IHG31IB!D4{gyqDYGN_Yhj~-Ngzv^lSux@uUuEI{- zVHiJ=!1eJrCZjQXTjrZ~z@XN)q(6a1hD^i!wdu&!nI3c>COwPzb9g>z51!D}zwrZb zsBfdq_NV3Die%QwjMin|BEo>CmrJ|*jCnw^$2dge?U%aS-za}<0vEFZ_Vd(sTM{VB zf^I1bfBJ}eCvI$6hB7cHom14~lK<{Cb8fR}$NbB<;>CLxlw*hNyXgIj@HgwP=k}@2 zxM)3A76|~Ru&+tt<~M$`O9Ji)SuuH0^({GE0EMTX42{-n_IH)szT+Q0YVs6Z+&Lt@ zTrF}W6BgI}9Ct{l==d-s{JA`|kmr!0`S-PCyfoI8vx$X2awuSy<@SMo*>7ptKc92K zZ{S1P>fhOFYJDGvmc}G$Opl<<^;B5A2d)R*&cgM8S4=u&4{7t8(&ZWO-oKJFO*T05 zQdajQ9S9eVuvo!ElCK_{cJ~hk5}`bwEjqS_h6Yfu02gmp;-!f&=|VBrA3e7IhqAW} zYAfpEeuKM1@ZuB+F2$v2@j`)6G-wMHin|AQiWV&{0Rj~F;0`TXptu)@B1LYV=XvLy zdq3V!nK_xs?3}&NI%}`>|NXbfW(dU->i9};H%1@JZKTT>+zHVp{~*Psj`l17{D~## zA4c1ZfAqAb*K@js6?(cmc#NarvgN#P;F7xD9gi%gA%A$S#ifs`JFE3pZ$%JvG?eel z_E;{%eSehL{iyC7_;7Bh?|Wr2)8D2xpRewv-59 zdo^!|%)^&K_+n*YFA+do#eqkpfX^MHZ2h@}K?b#9tqrgO2kG{V|3g*xy`G;`K%=kt zWg<)y!IEEKMQ%UdrI3!*gD29Ou~+i~jH`FOHh9e__oI;RA}BDEsWY z7J$~~w)FVVkUJspDeAPx7?-n^3y4?%a8}0W8=BOWWxCUTlhvH_^wa-BvhBfe($F{a2{>I0q}3+=1fO6>IVTntC`cFIEW?W7 zbs(yR48I~rlVT5P9T!+#~n(DT9 z?)+ExWj^un^c0x^NVt#K`n;c{x2^{aKJ52#7e)+ik@SY}#MGy~#O+-ecny!*U$>OH z$a)vy)(T*EH7wp?9};p{@>w5#Eq7KQg@HGIF7^vP;4P&rk>H&cA0RSN#k#rD^5r4{ zfF-i!&b&mf6O<)3BzeIhR@aIn+(G9R$G@f=BZ5TFcB6ikH4(@)PyWEsO&e&Gc_dngW{7X zBp@ilqjrpsV|Mb=fDoFtCcEHnXdYyc-L<)&NW_-H7k>SXd+6q%D1`7A#ha%BA`f%^ zhsExXw;Sh~_pzSs#s8FQc_UD416)xS6>(z83$^YeU+*7>)P`IQ>tUlL2^dpLWL8Dn z9rCe(+W9v32-&B>=foV_xWoty>dRb|BV8;P^jIlpPQLUEab-zQKqPetjDhZ&Kz-HwXDMAQ=g6KR|BI|{e zH%vUeUI9|Lmbq7wt!j&)IW~%O9S-RLVZO1I6UmBGEl`8(z}zRrLsmh-T$4sQT?gi| zE*f-tC7Tn#-?$8xhIQw-NIu0Hd?~l6>eX<#>S?F$@q2?9rb-(n`@;k5)xeq+OGi}Y z6u!H~`kbeeLmd@CGe2{@BX@S#hWj);&h?)OI8UUI5+OJgUKYLs-C6X_6deMqu&fml zB{GEw(Zovy_`aUg+Y^Btym9+RRU#aZODpHKBgUi#_N=*nMV|gKez+AL$4_H?H5HhV zg!5M5Q5-bG)Si11{HuEiO)*XN#O&e#5g@y^_FRd6JyxK2@%ZSRL7~#^AIOpXAWy0O zq=?0qGV-#qsFLw0Ccu>GM z@Qdt|>3+=q2e9EB6=OJY8rTN_a%n#JTjb)G`mpF^lGFig%-m}W(r=U^OK0=+p!^@N8gx;E=&`q;?{dt9cW z$U>}@O{bWFKK*8No&DcME&P`3lhREKv?BV`$@gSlT}1#(fzVD$)0b9QW}P1sLK8PS zK814yQ4qJNn=w?C#HUd9pG_2KeH*?T9TXDvNQLu$!=tX?*i*+RiInN^D=I5;!n0tt z)topYK7j4iI~kj98_CL`3m<7Y^oPSu>clC5b3aMi<=Qq@g&8KHN^Ku}d12*1552h; zFax?o%S{vxrH<;#t5FzZ#3Ni zXU7K|-ahOg-}Od6c9?sOmN|RSNjtQ$rQ0qb+Bz_3%7G2$XC`2juTS@Q(8lA3D`J}`2-4SAe%$Sk6oPovX^(#H%fEUb zVa5l0_1XtnvB1MHUP6P!--d8N2I~7UihR{k_BcNB!9q{}a3g5O;s8MNPMfXVGhfKS z53c?vT14oDMc;3b#1AY;TJF7?r*7n+_%fHYG!7n_L4oxIe>t=<&{L7?u!OEC%&Elt z4OrcX64FPCG?2KE;+{<{uj(J^Q#MfYwq)wr76fr`SiXzp@E>!pa~sSqk6JM6O26c9 z=14I}H7HH#4%X~T$;XR54|s8zB|vOIS6h9aEU9`4)+f}&d63#15@DEbSR9m<37mk{5Ui0;Y5w?BSiQL#@bf-&(Q=Q&)4@3Jl<}LGw7ObZ1b@v@3M!+ z&_m5*#`4z-dTUeuH8}Q*3}-~#DSpbfvvv9&yUwub2c@iy@mCS8Yob_A*X(Ve+v`Sn zImNNh7cR(~*1n|DPz1~{YOCh4(vx4;t#J>_J`TZ_c$LkCB|fp>Ci&3a$;0ry4YG9rNb1~Xaks`}xMRlZsHE9@SpjWKg!x~HC6~F7ocf?E zu}gbHW@;fkHtmFopb&b#;Bp5$x$mm{F$?H}%==De^l3lDYeSjGmQ+!#SeT^Fzh z&~*`6<>r3-jqFMlYM|`u+VhpZrdOdFzfL9Fm5!P6qk3?c6a&*9Lh#;RXo{SI?ATa+ zg73>8T`&lHNT}%>u@rs-3-<`JETs!p19Kbfze3ZKN`#}jg6zB@xEc?Y zE$2l9Y(s#+EW8Dn*Y8(Qef1b+T%`~a=ROa{+xv=Azu~46|05YZ?xYzd{*t>(L>zk_h`cUv!=OjtGk3@w!#c z<`vW9uoYA6Di@1!MG4|`g^FWLL0RKIHIHiUmbv+GlKsAFe2%D3b-_6azcY=fEI=jS zME#oBRd)ta1WSLFk)u7domzvgSqPzjsvRWp@*p5!8>7(q*U|dp9OvkvRst)%>@X3S z#^C+vHaLYs_K(sUk@`D!Gm>;kvba4(oTH0&1u|O}uHpCSpUc0M@(o2DJ9-~a2AcI< zEKYvU706H0NM{shz>SRyv(mq6Ka?%BRnm;)eAQ#-=?5hY1w;?HK!;>;QWMY^YaNJY z_wOO2Vc%ZvWCWF_#2eSZ^&1*t0xIt(U$TnpZ_jb0_<3i+X@Du=)As*GZfmeb1b zZZrJkkC7EKkAXR%I~URYX=OnOCTrYty84)S)24XA*WK~F@v70a1UhHBv{XfA;I+P5 z0#E%W0pa{;I`3}SXgBLScc=dP$`9?q)zjJ%s&T>T+~Ed!!)YW!i&D6}b&WJA0Bt|S zui>fZ!&wcH3e6m<7G{_uBgl)zpADAE$C(vAj*akbss-`PI z(m3w6IjKg$INW|f;6qI9Hvz{jC5S#SX$;?;$7ub8TL7d{50bB#wf>gB*KwUxvUtE}WVwY+N+Kz@VaE>&VJ)@|9*P*l9I1G6Qj# zyI!6ij%6(sy!jQ5Yk(16)$aPocNzYw|I;2}R+I=#%{@}fq$N+w)o|_uBP=Pp;4Juh zrhW&?Jde1bdAlwGnSuNF83#;vdGj*^zjk<{+4gd;O^QRtd~c zpS-n7?3ez)582}Tnz5QxcpEv= z_EKJ+9oWSw3lq?L>A#I;hbVjrahb^=olvi1H8dx^aPeZp=z2_nGm@CYtf6tAC}RyU zYdcu!Ky3>?dES6L%GKtx1pRYXm^3$L?1}NTmCZp06i@1P^ANhBKt{<5=c zPaenN=*R~`k{;i_d&G@6ynln@mJqZ9&RJkIj;cc()*N#C-3!N^Ik=39XKLW4I=z}R;&c(5Jvo^oVSeP#Yd z!4JNu-yp+qn-NJgj>V@03jSYh$;ova$DV%H%NgNm!>c>(&>m!(I%2CGdqNAz#ld4U z_UKKWK&r&mzK|>I=L&y2eMJnkh$HMM1vg;xBj5RM#w2i-+1dX6+MH`@15YNarEhwX`9;e~9?T-Gn#eUz1N} z!ec9ZI1K(ei{yjB5A+pCL<#@WpV#Co7&muJF3sc;=Nc9sTvvY&K~)BKTAD)Qkqr{0bQpaBX zqRy!li9{5-S`}D*n?pl&F58wt>JcXaOK0ev=iiG3k7+&hIT~U`Kr>YLdGEoscHpao zqoL0y9b6y_b`|WMCzq1`PXqEsBa$+(yd0dW)Ubb=B_j4i#npv{w&?;kWEYts>Kih+ zRiZPrIcbkPS0Bm%aa%4iOxB~`$}f}f z>Q8Yj3ag~(ZnzPu>CpJ)E&_lMf@En3%TQkLi$JQEJ>!|O4y(H*$qdTvP_NqH!<#K` z{MPIQJ(W+D__0-9I8}!`q$9bFfsZA~P4Q1F%^R7V%Gq`(LpuvY zdw>nk_3!m{Q$)!8<1uJ-h$r@bMDXvQ$FyoV{2_CBS8{qd7YC`F7_ayI%>HiE`&qXn7W{(3ie@nN7|KhX$+58bhoAD~>? zHv+$D=RgFXUpIQ;ki6M7GO3x-R7vJrYK0S@8zr~*KY=zfN1l81% zWI$0@=bhLA56JZ?DPi;y6GQU_-xwUd*1_eO6(m5O?rRQjNPkj#eXWFWLaCxO*yG`W znVXT$N7l&s-Jo!z2D_*0;$-}CPZ#`-kkFI%Csfaku*<>8(70IK563V+dplfWX>UR{ zZ#8Xw7*&@>VWC)={3YB@wWov}1EaDwoPxh13a~eQEDHP%SM3K2r!jrUu@h6(%qEHp#BHWEc`PS%Hy6?B>MoEtM)mI(RO>?Z8rqLvF|n79=@EXb^P^1L8x3 z7@bA)192o((hmPrZC>fFZxH%TPs%2B4gwXCYiMyY^CAVMjSUgG#Sy0~Zzfh4u(?yd zt{Z`$ls(@i&)^+Fg>=Om;w#k(%8E?6Am?V_N@+X3Vi1bmw>E=4#<$Hvl;!~Lvc-os6mQm7>xZLzkxb42c zn%bcEJog_DNS6A@?bU6WAnXz~@aBe3-{E%WWt!pRRc(IIA!u&qv6`?VjGkyY1peVU z#8ct)kjR`9`|)f8O%C59s<3b0^DLy&F!qotLg(juWA=c`4}W}`Ha^m!_6bdI`Rx1P z{#7yQWHKWy<()ip9b>u2mxB1YFGlFb|=b(l!o)TMfiI2R7H?>Y3$)BM-@qQ`Bj zeOKDu;CSi&o?HF~@&y{e2a#P-HVt))at^OJ$(K>_m=oyZcX8*((NtKqa{HcQf}Fwe zIuH@=f^H@)Ym1Z1T2E{iU+y;-M;2|A~MAZDGk#fUPIpd8FL=dGE(S$H7MKX zYF;B_vn8qc4B8@vk*(7oB(G^L%X}8NLJB2m?TDuiRLaZx=5DV3>O5rb2OL<^| zJS;`~Rr`t=$n#XRCiq_+Z-DZ>j*oCrO23tEUE?2}awq#!gu11|&zv_0cQXqjC(d5J zMIeIko}dhN&g@n(L4;;L&8}x&EY?qnwUGnQVaP9az^`myK{^4{X+QPIsBT}%b38fa z_$ob}U&D(ZXv39c1Nyt8X}5kTY4Z=}58x>y6|cywaWI~CZP&oX{U6Ud#kYzY&u8HTS6YRqgIzi{Mcqa=%V zVm3D_Is-BM+@_aNR;7oUN%Gs{-{PLd9D zs*>K7QuXkwh^Vl2@6QDlsYpFHBDBmacm`Y7L=@k&P?~^imkTdDDP}Wee`YaGAS~D6$u;Y;ow(((i+7{tS_MEfvtUH6Z9^ipI4s(Nes<5e_f6^Cida z)7IR34X&&}=1CtIpf)XeZwlH~0)C#Ttav3Jj+GyEFoQM^A&1goVimPsowLH<;pG{> zC=t40G`O*1PC=Q>79pAh%?nLpX1HU?UfTBrpC{vyJs|F{%8{OCG~ilXkGsIrT)Zk5 zmxQU^=_JcWX{SN*a|Fi461E)w2+@RS!DoXdZ;zE~c|>26sZp^cDSG>sV0hgc(w#W_ z0M#*ei9EwP15w|O=T(Q+BPHx{IWJ2F%$*Q)kE_KVGX-T}5%qka=K1QmiMmxmB-Vg^ z04rwPvQg~f1W7J`KL!;Yo{VvPrS}qFN6+5+Yhl^H*5B^AlZO+lW#>Ez-Rb9M;5X1) zD5qs0DxS0a+X(+%XvgYFZiiH4kHLX)d^e$#s&{dy>EM~-uYFlKMD*^w8aWN=WFOQF zj<;5SC^uY(r~(1DCUivE@o0$){!dNuZ$H;XQERWe%=3!=k^9iPIHEvTJF-OS8+lEB z(WUDhQf@{(jgdVh*~ARr^G{#>R9#juxn68?kf1N*-9AW-J2t}3qm4oycly`32>`T2 zzjZ~ew{~DHbV+tMlxQF93x4m7t%!cRthcf%%|0rp66m{(d1l~9sa=W6GWqpUix>MZ zb6>eBH2|N9+r2!xU*ooIFgPwILDUaWx+lC_30A|lWflx0xBeX zkE};a;$4XSf%8V)%dtu~p`T2PuSbw7$&O=zm1sIX zU?kfi!y#GE$A9(+7-PW;=G8qsTfbn@cu8WQ z5niugj5D&CI2u~18BF59L9UlBC$y;^EW9TYJ$&z~^Z8Ydvnk^q=yPWpU0;>qJG`SD z|B*zLg8Tb4I8xwm17=3|Z$<4W6@5*;o)ecl2#in^JZY9fEFda1K77yn&Y@TYZheve zH%+GNk@s73-r1KhrV^(%3lvp$)@5=`vn{m<{l6vAm7A^Qid zDUtiXZAN6Z#JElKds6`x6NZZo!#7@nt^D)4U7Wh^VxQ7B{~IVn;rpS+&Mo0WQ8fbI z$B|LQ=NB>Xgr_37fe!S~FD~Qysl{t7MgV#deZT8Qiu!$2|3`#Le05-wH$!{?5$oc)vv zM+N>&`otT1J4UDQx4w8V6uG5+c$Y24^qtC-GTqEwmZiEW`sbMlaYN>G37vvVYlO%x zU3yLE6Z3!9sA7HF_b*;jOD&9>bxwk7^@kh=n4rl@`#@8$>TZX9IQAse<^)QNcOMF8 z7Gtci{@)o!xriHlucU}7IqIsGW^rh(K zY*2VT$bAm+v)AAlw`hw|K`(B}PA>w}(`ASXE9(>taj(JHOV;R((7<+1LGWifVDu(_ zM%;D{9Rz@LQxzZ&@RKs4-$b{YuHiI!{R1?K53MV8AD3&SO2f3!Bn+XBEXLLaIF%+y zZ-90N2Z&Qn4P#9fQZGXNr;9%BO3_8DVWE8hW>8rUlAsU|vPgf_n@hL#XIC$tNlBX~ z2lf5u4RWLuKH)XX`w-1v!BU4mW2_{+i+ z1L2|HH*!8=Y=QHFEJvyThHo?0%(^hQnancYC)$FVux_l*(rYh>ALbMGf2IcJ6Tt6$ zxd-M&svZ6l?4RzCAIJpY@v`-Q+>T@rbTjPyo|8c)@K)|K&Om--hwpwG-ZN%b(HFB6 zn%gR-=dCwkt)piIe^NL77%S81ntd*RdR*X>?ee=h8FD%;^OCq*)mD@vz|*q__-QGz&B8p}RRxZ$%r6}ruSAq$Q$ z+W`2JD_Q}OsZbG2PUW30-_c1fV5--81#5^+zZsh_a{Q)2V2%vqJU^XB{#$@vZ`{08Cu#hLyXw5~{1k2$IHB{s z5j*LfIxcB6`#=RSZb1^uVKT9?To6r<{V=AP=|$K{TpfB(j1glU@p1BycZBUKQ&(ef zW~|jf;*qu9g1(oHY2-(}%bLz;l&zksuMGaNM~8w;`KX_{MTG!yK5=+|HQp6Y;urj( z+0T>|tE;T>wn9*7(A!t1>vINw^#SgAu$6N9;3hN&d>)drFo|~Z_xh^=2Do;8YZ=XC zpd9U5i)Z?+V{%`X)9-*JF6Q1w31Dg{1&}{j5NQ*=%E@)i+0tLDQOX=S{aM9yig`@p z@Sw)&P@CoFZEKYHImCP_&W;Sp1tv)c8A-YuWw#`Bxk68w(!9iIXJaU9@kcS_>>Me` z(bou~6=U#Jx%Y#VA}F4a5#wk<%Op1|u;p3b?`X(82w93pF2Ahqz-G8iU=PFkYjCmk z&lSyj)M&6SX$%(@DR;Wpe3Zq+t0Ji_si6>?qK9fne0mcjmbPS9rI57oKVvKK@?Dl>8kX^_^{pyK=hw~m2q`7sAJOAYv8OmMPPPh{gU(OM=C^KY z%t|c;x)i`#C+jHpJjiY%$}>BJv)&muHWk$Vz%`oxY#J_C^L#*UkioiowB~>Ew=LJN zv8d_hPvBX~+0&D`?DLiS%M#u`-{(Hbooe%wpxYhfwfnw*S{9>rl!!tG*`O!Iv@B$h zxVkGs@qf5FxZ%r}2`KJ~)LsR~@RlYfY2OQ@PCNa(`{JSx9(?Buz6uSrRN!Yl7%Mpv${ zqq6&_0?`~WOE4#$#~!XKicCq{&x%D?O=vv05EmBNYJ6sdNh|Hmi{yo>P|PB9?-j80 z8;A=nuy7f9Scnupyqny4%^)%#+gDfs$ho!@^bFC-cT^O|pvG_xg#tDf>9kSFR7W+M zjHsyr1eB2h5Ds&UybX;^3}aL8O`NWjndt|oq~Zr%@HVUR{u%Qa?r#_lpdG9+8!xAF zp6yZqRZ4NBF-D&%p7EB|qQs*uFgO!czgI3@LQZ#QX)wbb8l>W9Y1W(Bk2}jB(~&6U zjNggxj8Yo>V!xWkMHYwUf)UhaP;K;R0rxB58?eu?1L{8XzTLSyzAj<2kmUftMO*3r;N|{rolBsqA=iE35!$4eVO;6 zBKEPS_Te@ZK8PNpIPM2s6u*LHh8)Gu1hO~~4 zw!9x0t5yxj;guKyFb^{<9V*F_EjnWd_o(MIINeM)bHi=jFI*chFU~}s?5rMelz441 zhQzL=VO<=)dYwzJ;(!4(wL>z#7A0t~NuTHR#pufJ|Inm1hff5VD)WE($0L|C1TfZL zxk^&H(W(djIqC9{8#HYQ%|J3*82N2-9o5C6NQmt?Mk)^|Yz$B-Ewdp>A*MFutMamF)yC!Ifv#z{}MHih>N- zV}V7ObMJ%o59A4|eo$PdpO-Gt@-u{4o480P2kKD+(m~-%!==fzlrL&4m4~SYzc15e zvoqSc**GQgVop^BSwygL2R?_vY+k}n^8F0fo_hv5VcP*b=YYGQj_%WRSm5oB>C4g+ zn~f>tGJrv^m-)Iq4(4k|jZXQX*A-vpBDK4NnD_m!Hu{ig6yo;m?Q!6=(d>W7sJk0& z28oXWf1q>69xsH>zDfQty~{Eo0a7>E>t|vnMOE5T^FloeW><%?u?%#O4hTV{^KtRo zQvV~P)G((U-IXcE8c7YjYz7{<5rS2Nfd8OTO}hd!h|xQQq{}@Ljk2&xh1p90F?UxA z2Q!_eku=VSkMkXwZd*oR7MVUPe(C?gQ7e0t_Ah~7s5#!blvbP%Go!oQA7>ay8C?v`_ZF&qU$!`+9ER5c?R}Fioip; zmwK+%c|h5*$@v0#2tKkY+RNq$(sb|a;*IiVt8!;kiojwZhoGpy#48v&#cBa_DYvh) z8=teWGb2dGY)tQm(+bp?CurP`clC0y`Rfsana!B(_g9_}IomF~1F9U;Tmb(ATq3Ir zEmiO&>|{&vN2g%Ngeay_-Qtav0b&bwb=l?;bu{F9T*V#fw&Pgo&Ik*#jVLiALY7-A zms?MtwqcUK&kAhksxM*Y?R0+@CXHpQKdbW9S?QOb1Uidn zTO>g!#Em&1@k^w`l(7Q^JJs((ajl^kl03f+E`qrcYDx-#f2{57`%PMDaMf`I(%}bM z#He+23jHg3l!2UTAl9JT4y(uh`rHal!^Io$q3D&shZi0?I1cZQ|Hz9+RbvhKw$f~b zN@o4_S>$EgKRfiyWj$&uT;xaF{j1vUHp==YU9dlf!oIxMf`D7x;4y8CK(Maaz%hof znSv>zr7GrJzz5kx?=E`u)RJqCcRtEU$8=7@DYQij0hj?V_R}{c37&MsjXc#tKnhVRbV=cpUL4M(QNq?X^GRFQ%{q4=6zU zv|kV4BtdhYTL!%r7E>2~x8_??H7I^kAsUh@!d1k=ujLPw!!(WjJ^YywHhMKnR(GyO zjpEMY+WgV9po14=HyK+KpZ->3d$9*C8BEOa!?(iAlKQH*E=NkknwJp$aLMYzG?Udq zpzMV+_HexxmIJu2wNO17Ki}}VA78!PhWlDg2b&G_4O)_D@W%*zdb0M2Of6Rb{mXCH zI4a}K_I*a47VvxsE%{M{R7!EC{g)8iVYOkuXoL?(^5tJ|@DVBhTglay3ApRrYJLQ ztH4Yf*$nKL(CzsVL>ocyZ`~LZ6`kVY+j0WoqbH*Cw zfXOd)h!q1s#}T5QcD|W}dBeY*hJ?f5(S6%LB+tgCg1aG7t;s5{x0c$<3O_uJGoQ-Y z2Uo(0n`zqw&*8#^{nP52#LqC-iR1ZTwO{<{$@53ZD6gY{$7jE`B~WVYR+~?s%lqhy z)ziic>ONtR`dDaTDXEEFAT<<_gEMB?LBz`%l&Na<+t-~@aglQ;)m3$A4Oy_doG(WIcB8cg(w2pUL=MH-FZ@JPvh*SCN1A!7WBytS%-{&7%@BjO@H9GO zTn02qN!~O9_dA`MsOU0Hu%9VdYwVx=k{nGw)Iy?+k|{9n-Ro%x%5 zsLi^A!T1jb2kgc1yD-GpI@IqK34%AY$!53?-|{VA%G{U2b9~YOGL&jy)-AWD*PLEe zbCsJLv4}%teTWOh_3I@Ph@-_-$KpOnCTQparW?Ulk)+%7b`P-szoc8(@U{9_ntMSj z7bv)ttckgu!pMNrRUrI*O4Bz^!$`Ws+jg4A{UV5~H(h@$z@1-4CsU^t;wVG3;LM;7aZuz4VHUI;L+A!wl}i#oux|Qf;Rbf><{oWPez`j5Hlhjr#%(>Q{LUC3%r( zV7*@7?F0G$fW6nu{jTrz#o;x{ntrv zK9Jw!?pTz!WY?1RUVK=A=}87`nW^{Z16YHOy&?V)1hV4u`a&Q{e0%*Pc;O6n*S;}% z!oCt%VC!sLVNUwuG(9bcD4as7I{yL-!UUPy=+R%+Yi&QOp_9M%dp6h!MS2}LFWUYX zj$_=OTP83>3RWdokuCfhns+D-$Cw_AC)1`4x?!ksIL)7#V2@TwCq|e2$*h*TQ~tvP z?%l90AqblZ$dJS*uPTA<@=QA@BZ(V=v-CKWMSRG|9@KK$WI_nNHVbI!Gky0@9_8E#Hi$CC^v``X^|c$m$G^0;_IBb>uS-=F zSfb^e@vkq9cG4K!6_k1uoMSQnqXbhS|I_!MblXZrU0fA^8Swer;rhP~PN?NJww zk^~3pvs>a89c_1bXJUEj$7c5E(aYMBJBS1ciNFt4XA}_1@tKz0>LH4X!ON?r8m2v~ z8k|XnLQ;g^r!SlR8?HA7S3P!Y54fdMb*9yHK3qur2y^uaFq?^+EokF@ZdQItqb&9LhQsvsnUw>yKZY+!~4Q3N1C%VmU zMyKBzV*K;xBlFKvu4(!C%adtN=rS)r<10FVz$*=mzl(deNE(LBcploJIo$@;8+%W( zo&S6rF}Kj>@11ayf$4uJlfWebBp>Xw$`MQCut41B5|Ezx%Zy$BXp_Jz`Qp7fu-50 zSC2i_(se#3N6-$CFgY+Xjo=-W~ zZ_d$r<4<@0qL)6$hcVxlxmAg&eMWkA(IQLLj{%4_Wb44o0=5+$FWbk{$NZdV9(0ga z+${Te|HU`|tl2#1@w(Z3R_mtr*TYpM9J}|%q8K7}f&Hi7fHFH;o-7{AdzJl~Blz9% zfv^050>7qdxm;y!Jt8@lX?FZF6~D$kMdgK8?$b#+mRQ9b6z>YO06aAMnqT;DJJnzO zc0{J76_W;7Aw#x$FEu}{#t|7`uk~D8M83jx`yv({`_$DEntf$jbn8&CeP~MPVA5yI zn^E`AmDW>S+mLu-puP2~Dg_zps)3wuVPaIx26dL+{_n1t`0Bs->JEQ#`Y6@}&vweJrl z^jEP2_&O87*qI=~KALn@0|tAfI|f%W%;E9z_OkZozQIWLcXv?Z=KfydEn!DVQi?Nu zsy{(e;jdet0SdpEH_ar+{V2bjYVFlRJ5Is^c?`%uFJtnLepT-h5@0N+sv1IHJmf~B zIaf)-si8vqEw4HsM;!a!w38Ae7>-2x*;j_de<#GM%_X9Unlz~BR?&{*g_b)vnHn-P z%7JT>DxPXV>w=l?@<}dZY|Fa=)V6U0=;l@$Em#&t?YHU$4RPxZq|{q9zuiY^8TPQKU-*MQ8eg+cYB=qNUXLZJ7Pqv?7SjhyIUcuu!N8i|e>Oz$eauLftw=M+=+Lz8v;XPE zIQMDqM=#QSiloto0|WUjB8y*iWdywFBTo?Fyns) zN!x=GST+ND|bq|iwESR3f#9q{nDV$UaORb3(Q^mhV**L@JyNTpW8&yBQM%`MMq)|%A zlZ_Nvb7rL9N(jyWp&45&zx-Nb0!pNPvm)yV?ObK&d-X3MEi)HsFF7|_CZAnBwq;Zz zEjTtm6E}gK$B9Ad>%T^{>UNokJyx;T6Qm4g9UGCA2e0KrDg*`k=sP=+a<}fFnuD6I zQy6J=8J{fD&g+yn+{T~3Z7r!_pK)~(tE6fVHE^Hsdg$IY5L1;>ClWKWF%V>07@lUR zF9Tb2TX(Q{=R)=xz-o!5faNA~9;Zpb15((3H+&QE;+>)L1py-zkM#V@H}%hZ(^Q3T zGZs7k%AZ<$zck8~!5+(wJx=~F$Y@$m`hP%1n&JNgGUC4o*AKS#w<_2^*ZVw!Hzvlp zgYeWjE0M2%TCTE9>0$*<3IAxc*)Q^`)7syv)z6X%asD;m7DVz#60$G9v2%|!$mG?G zA#ov9&x5h*V|}WEJJQotfB6Q#I`}Q&Rp3Jm6zD^^qTIeIRg=3zchWtZ<2b*jI9dru z2v;l||Ln)(VKMUSv(-Cdbqny?jD+BZf1y$C62-~;oC5!4bcnexVH$T&l8Ebpmg4*C zLv+!y&O(LN-}3#xQCWX%Qd#r548KL%F@Wh3)Vf~zQ_A%lD3FluWBD(RScnt8Kw@Zb zEu-v>HvAsl#U4$>TWQ!uRJipQMrXb>68=-4@Hh$Jf&FZSWB!y^vG3nQxXz{K;*H@c zXyd3$$0U+vQAOw%C&pq4v%!Bxrjbn*kN7H@lL0js28KhJuiq?qvB4ShK)-UOm2TzxekF{JI_cl7rcTZ0lV=+bM zGDm!?`v?MhhU*(tihKg%&VMD6e_Q$1Z{B36&PV1^>&Fuzvv%akhPTLCVhYl^P4#XB ziHw-nCj;lfzc*0b{H_3rmu6tqqiQcDaO=ASj>YYS>f5|4#OTuN+?zu9{VxoH8%)G& zX2Zb-c%{#d6b{~GgfSXK-);8;giX6q`heCTE}>1`eJGI+$6@XTS1%E{DlISiRIm;? z;8#iPj>}7E6b_?j^|T1|4X@Z97FIADyL#_{SS8e2f6klnAQ?~Q6^hkBI2gH!CM|o9 z5s7Fcd;R$u&{@&4t?vORdH4DhWjDpEmyW*drx|uvU0cv?#jcywm+ur`3hi~LH3%4+ zP*OcQC^`tw72n|vhPQ<@yespNK#^8QQnvv`?Bs&u%~lg^Jp)> z(ec)h_miR&bH7XIFDUaKH`ZR}OBY*xw52e(<75UEqD*J{C#^KSB`qpTog;cJS#kT* z$)m1<3eI%-W?=;{bSDN6LLyBcLSrYpDFjK*G(2d9$5<`ExQgr>4o z|AQ>L%nfh^>$j8I-z*~!NvN!=4o|RerOa8oUZdG$26zh{B`EgX8iB8R9Bwqg2PWmw zUe7CDI1Wswtrw}~cb(*ET+&=V1J(gZoncu`i-zH+s|BsMr%0beJ8A>N)zj5hcxtXp zzyl0V#GU5w={}ffb=NJ!=&k?l4-DCWr_by#-oFR1uW{`=&VD6!u6&DB=6Kwz7sZtG zUwLvv;IK&nHEtX_N5Q%~~ z8U!z2Kq*FWc$ReON8Oqxyw5-&4RA=xr@T-QqZ+skXseA_4jZ4AZ`SQ#g_V%+wAga) zo1H8S$e`j%+iY$_ib8oB0PNaZa_Z<%?qRlKk3P4TP4l~*DMwn-G92G0+4C|3#)}i?QKb^e=R9ns8E*e~m zyA~)O+`WYY#ft|I?(QDExI@w68X!1DgIjSe(Bj1kMT!(DC-3|J?sw0+>#TFny;*C| z+R4t&%%1E$Kbd)+XYlq!l_uC^4y~n{Hp4@ogv)4f*F((oJpB!H=Ose*5z^{3Vh-c| zxiIb?v0ziojbiu3A{?nrx7%7JoMC1l%_D|E?M0No@zK%PTW8CmyJAroyZhxuw5_~3 za^Pq7lC1J~975{;3_z9o+3mVB@Q`wmqZF+&dU-zSHe(2OCt%F-APt3V1(EU@?VCsthPjKd4}e;p z(!19bA1JIw@rM1VP>fwh_++SHdVKlA)F=QGWiIY2o9l>=(4FgmiSKC$?uBZi@!Mdv z+lP?kt9YXEGU8>|Zyrmo-+q`WtN#kUQ^QheFt0{n7roh+%VE;QChE~Of-0HWl#7GM zCpUzM^bCG*YmV)Rah#7Ua*kD+LH58zn%f8!cK^aFagIJooXL;6u|!$*&yE$2dF40PgoIIAAB-3Da6nv@)m1S&y~3 ztar=|U|V?+3TDET>(~+rCu17s7k*F=SWtRNX<9O6h9aJ@otVu#^LdQKKNLB*nB6f{ z8kuPSK`o{s=1<0?bXmr>WkGc)yRYWMU^K|Hsy}QULDkBtNbL?L6phjw6CcA@$#BNb z4ij^tkVdqqW|z>h!*y2rra>ns^)Jp(deejBJGZtYYgC!!URi*LDu7=Qmt46AHxpvx& zsAQ9RE4!JAn8_+l!+*B`M@4zO<|`(_H@b#f5)^ZW1J_4W(0wE`BJUTxKLjT=w?;Al zOufx%ULK@0t{S;XLMA_`Xo@lXp_ZBkB`yu^IK{G&ixELcr(3d@UI}KiWQHQ92+lcs zqeozgdbd(O1pSfa@~qx17P*hY?<&g1}! zj&Mx}3p8B!&GvD9RluJYfpQx@XDP9u!J4Z z=&S0SRvKCJ%bCqU@AW$b<+h}qZ0%ar6;CAT;oDL_GBPn(fWKWm4(B3uJSS(Hk@gHu zJWR*`$9){^zNAEyj|+McKj3j=e5w8BT+anB?gMy$*zBlyD^t3~SF-mJ;i{CR1xd4L z7O^ga|f1U@;Mesl31Z+N28)1NfSes{Lq#2t0* z5nb*h<9&i4+6TqGUA#AuZMWIpr_xI%c%P4h1MUXR1>;!#tV68FM35@(-_u1?_|8Z6 zZoURV2&Y$5!FBOzW>%oOXz){3z1#fm>VC-gu_AbusMzmOUroOO2YU z=OX_Dv-wv}GQ_mU9jPUvfq$k&$+I@AiWb~(07B6NC6l{Ya#iZj_9hax1IUkcu+*#9 zN3vKd-=q!%Abg5g0`rcxu-auB?D&bqu%swGe9Z2Gw1+)ekOYd=Ah9GBzGDvi=3%XD zjtSH}cI5CELJi3wTK-o@2=2jj?EtckN96l6dVS>$J(@B!ztQET(dkItiR-V=lBGVX zYaAR7Hk8`J49S#!eZOUuo>kry3yX5{@B?@K@+~lP`03EXPw6WxeYt^>z|%Yr>)$3g zi$RG90*#I19lk;-Rg3_i`C&jBE%lXSb3)D9{|=1EeN8fpSO*ZlEARcBBM7LypX7(; ze%p*c4v4kNH1=M;zs?TSwbkj-y?ej#)T3P2dD=xD88vfU*UzJ@`d%idCQc+s$Cd0D z_ar^U)$N>nJ0=X-XTjYxAM{lt>mr&9S7ahdlAFsz4J0ng&Si#$gO)9;wqu{qa~HQ2 zE94m0NbpD3yxe-Erb94{J&+n5T4D$?WwO?RDY?e-;qkLcU>`h2lzR4FLZYoXIuM}4 zCLIJM@f89fcjD{@RxV`z^ddY5n~|vQ@77@sjC+&$?byMt+c3+X0q910vhL$A%y|bR z=z$iNik^AMKO`9(Je*HizCzQ#teg;#L?)iWe2c{oV-wBgiv0fB*^jayr_FC*``g{M zgcU*Sb;eJ;T+p{IyZ=HGQR3^gFdy)3Ko0Lu>e~r>xu8J2I$5dkvsjEQ(mGiPy4Dd% zS>F5x2i`>vY}vwekLAs0kdgebNxK2npQBZmB)yU2*HTu9;Ay0p-bW|Z(!PB8Y%HfSnqh*;xl`+X7EZJJs(qxEFRg}anbqeyq7-FBV}a0%iOy4&%PvDPNdbn-W?%?-?tO7Bqm4!=r&tow$el6{T;Sw}U|aUcLUSCzKW$N)(l&YZqZ zFf)smot8~f)k~%aFJnZ0G-waREsl~`W+|2U94)*}gi&q3&~F%#5!9vsLRf1dr5IMm zqNN@_xsTVF0X2;>L!%`{P2?E2Dv&#v#j5XB!K89y|H?mHr2Z$)yCv(=qTO?gm7KcQ zIza;$qAQecfjip>R^^t;$8!AUrAhzFmD$#5_ee6+nLBa`o4cKfEuS=devmdDYFHhM zJjKBxf(J&5vw}*MA_o)1k%`#G0S# z;{cP-1o$R229!n)2bx#k7z^n#Z_$1{HpI+zA2GaG()V;gk-Ial`MIZ($Q+rw56C|gS#X4gn8}NabH;Y^msQHb3l3;EpqwcSkdllk{oZOx>b({H@#)L zWYQ|LsGRR9?#_?Tr&ZLef7V1KeMM7~*5=aBd97^E7b#(Bo)~hArJ~0eQ3t8op0v3) ze}MR1(Vn=D1yl~=rZcd=NkrqDnr&~eaGqIQoRY8tR&hg#n1PS&TN-!nHwvU&cL!UU zzML4LBC={lcBVtP5h*eTg12#a24)nLX50FUp?)SGLWS~j%2e7UQGW7 z{@@Lq8p!o?S>mdBh*Q2E*@TPN{1qQB zw4U8_3R^)y9YJ5n1PQ9CR-PxE5=h62V zKUEE*i}7own3?f;BFnj7L3;ICeYl|*tcuA4S}phkCDFTthuegs3{M}HgiwbLAJh;$ z4yT7Mw+B>DMT=Q>=H9_f|AkkajR`@fjCXeE=xlwblRe}Mm$p3%1V&;VR{{&a;e4pZPcl@I+6ftq1XMIRvMwrzF;>M?g%<&rvo(`dnXw$pd z5Y8JYy&0MO5Tp(AX|rE9P+0nwttlvR<^jhN9zE?J`p_5adpaS7Ks-)=N32ka)t3fC zE9x~xr0L$(4*x{xN6F8;`^h%_FLH(Jqo`UGXpl&yruO{;0`CjITsHb{o~$g={q#Wl4z{XV(D>;(*r2D zq@mT;d|5+|`U;AdHl z$1U}-2Jl8gXNy^gsJA0XcBYH?@ITB{o(!Ap9huW)2@a9A=M(u zO(Z%sM-4SMAlWT}@e6M`&W&-#4WAj94jf5xnl8kccdvttyr;j}qd_VV=WVn=aA)5CKCcR=NSQ-OZ1D zSXsJZ^Cfvz!^mb{a2q(e99v3lFCX_G9F`=qs{bLm+E1=Z(ZTr7tKfGpN0Wb9YT88) zScs7*Yl!EbS0t3Y7K|Ph0T}2i>mq=Q{s`cr`|#Rt{z@FL@f1w^B{Ed@&&iAp;bJsAfA)fp`R7Mm;;~krqm}WS&{Kazt^Cbj{ z7W)A~Yx?(hH(in60|qqkqBA;=!qHylq&R6)_(eHV>V3^-p~A%ZIB~qx)=gU5_CkUM zGJIAfCbe?7eqy86Ez7oQ_(|qAOvuc9hH-Oqd~X1x@~2|9*Jq7m_+P*mn)w`B`*$}S z+Z5UvcXe)jM5NJxd2%R&?TE-joT)}>NdEPCUnkrjFG?L!J$4ZRdwuCN>UZ%pVE74V za&h3YE$zTB>ccmKim!Cu(X9}h!B3H`cX9)aeA%4HAA^pN^@J3slGFzGZzKnqR4HaZ zF3ln8<#C%t&}J9}r>?_SU8M4NugDJLy{TuNj!8hquZ5xqYKCe;QUQ!g1f8)uS7~`wQ z=CnZa7FGsP8qM+;Yptg#I7-Sbj$7kufnJlQslJB5IB&khL=>{CC6T z0?Hz~PX6BwaRw$sBpKO%w%Gy3NP>S(Kx`?SB>zWW?5;7+?}(=&H{QZ)bVf71fLG(- zvvyY%Ae$vNwBbI3*8kk$YKpeSJGz9BdE}$2|3n4&fmWgb40|iaAd7qqQFhg60Znh* z=lC3aWF9|>Y@1bVh;C2DL)I>5WAezL%d!IP%OpQ&P8y~iC_l7-Yr z)g){LNczrtq2Ti}@^J%a(syljAlBL)&P4%n)5XXJ`^#(k0X8SBoE>P}4Cih?Z#Gr2f3`#(Sv-D84$>G4{8-5=G+-@pQ zKJF^RXrftA-_tiKocKfZv+Uu%3f_LYQnnN6<#2g>?AHkqXk~%rUPloGm&DvqU4!9q z1s{08DROmnDt@Pmf&e~%GgcM-C5ZVcrHxB&wrZGTA&|aqqbxOm7)lCPp~NV)0SNRY zlZRzA3u~3e1d!LQm46X^OnjVWnLH+fZdHjVbtmZ6mW$K2d$dhtDD`LAG#v-f4s4!4 z)qTb4Vd5tEXb`;`r7j7X8+j-G!m7`PTe{XQbj<+RltrMZDmM5J1u(Ppy&SpkIv_IT zlbg9e9bitKW zS`RD@fq$XUYqEln=SNuo(isN0i`7u#;Kk5b*l3A5jKDd+GOP%oW4hm}-Dc)@VJ^)N z#}B>T4qB*YH}&qcZwYcE%!&25LhWEzlmqUx{%WN>a_jppX-qkntB$rxxz$+FhMGxOB?4dyW z8>3$WIJGE6$w;O{Hue0eHE7>IOyMQ5Q>N5#-8pdW*UyRh2~m7LLq+t&=D?+J#4i83 zAiU!SGQH@>OSVOOhT&-8-bh7DZ}IDE|kY#n?x)w?BMenzk5wfVuMbs(p?B3<~^27OYMa<+yYz=Qj= zOMtx5K;Gl=8uQN|GCem69$h<#!UF-lYT`u8>w4D+=8@Lq^l7h-b7fNGBWKw3&!@wk z1Xh-vmHst>^QhxhV}T-R49?)7`&}8wt@3Koyqqh|M3)YFF0ifEkWD;iZ2pG^V~{K* zhEs(TpgfhfP~HZqLIJ-@CpsRKjZ2=R^l!yv>s8!N_!UTPEZExi4toKxtMYt|Epv*z z7+w;94}X(v5_nOGV5EztdV_TOFAIp##Ngtk_RFe##`y>2mHxDJ-et>|S^U^1*J7al zH$KIr-q~OV&`IWM?wH54V&V9FQijsDP!rDJy=8*M4q{5QK?0Mfg^D)jZ_hteVI&3l z7c1bXg%&oBa#}EFGmOp)uOYs!Wrc6n#wUf|`i1P+OgjvAoTw%774YZ#Sg)Ya+nO@T zd~&_|FY;*cLQYGd+k;)1;1%-E(hLtDYG^rMMY1TEHX#)Qqr(7XnA?kX+^Tu`C`{V% zje>ni8}`1oCp%eTqClGVsXFax{?%(uW%jB5HzB-Q6Z4~)v7bdjbpg1`*FqZw>eSTw z{oppsCfl_IzAgm- z@C!Q000<#LJu-|((Zr{#Q?&LqZuX&F%P!F&XIO<#E?GxV`Lufh4eOiy3Op1Z2fXu6 z^!Bv+5XpNVvc1S0=~21hRkE`|vqah=YhX&ChK)0Eh-I&(HxC8}fQKJRa9E5%x0|Z}Zio zsF?^B#yR+Cr~kw>f>4r?)qT0ASR6%aXR`03{|M|CUk@>Xbqm8E#M8HeohpHVU+D3zWSD<6 z?abvl2B?ADm)tMhc33qJ-0~ZBb-vE@WAB9b(anTw@yn z#mRj#0su2z6NI0zvfOI8M{}~@DxDKS8S#*6>mBJ?TTk0`r`)ylV_nH<8jk(!J4J7h zNbE@k@?oM@?iuGNzkrmw)Dp;HyGY$_i1iM&cFb-T`qkc8dhb`fA;jzcVn1}L_CCXX z6%qdvV%kDt8m(T`qv2M$4)XSB(3BhH^{WWU-~9f#dh5wzJChd_fF!*@e_lMlpcD)) zV!TG)``Jw(vu|B=#5^Y;OuSyVp#sy!V>NHSj~eJG7?DMcPUqOFUlWW=7O!KE6#piN zjJ2{7Z{jVai?@f)>S%i@s6n;QY?Pp2u4iK%Kx1&0nMG>~+AZ3HeL@2rQeaM@&JGr@ z*!YJ)7lRw7`;XC`8S|mnSA1y!9W;Fs2tV%6E{tMP`qCwwnMp!7FMb_U;X;MWfHDit%musrL+>96I<}RCtlClp^{IepX0K?{%Rm<;g@A# zu@ACanS-7>)o#?-NutN4tTD!9hI)54UH+A&ilpEPu9WiKfMeEyLzqB zsre8?!%!rakli9?`I%w7-zEb=nYl*r$<%#^BV?7`mwfTOxB}@V%ZbVDk@SXYzW`xr z*|kE@Q{DvcnO~@!qBOvlSrUssP~ZYfj5N8b+?3pU8vR-{7PZ(vRq;e?e!FoA{MmhO zfvo;U+Zvl2r8ccbgPee?0p0wo_vTSpA) z_F~P6ZljW0OoXtrxFLr!xZq} zsU>PfLjpTqEN>r)3h|8oN<=u^-$nNz?O7uIYsl|ZnaM6e<<<6(#OY5F);13zXdjM` z4(oMLU-IKge*{WRB02Iq2yvd*9LC7!CM5TBxVO#6{DxZlhVTC_2SA@7XId4R3Df-? zOPz>1F4#eySnvl`nDQ<%hyugS>bFq8cxSSjgB1XF?F?Ji7~P+|=wd>QypH!Y=XmY? z97sC+*NXcnh4zpF=%dD>{*qfgiwj6Ul*?+8qM)wW1R)AI!dWON7O5DGG^`Aa~n(7~E>WTF8TZ^Y^ zvnY$HHGLO+?UsSB)cKPxg7k~kj(TAdH=~$dIX~WzR)}hvlNW)A=6WK>NG*u2(~GGg zB1(^LzD+y?o+x``K>9K>rfgvXc|FIuDbv#(i!AQV%K3tqIoChH-W@Fm>& zFTwEd6xHHFC>=sV^L7}f)s!id(lA8hx>YBqmRDcHQnYgUAyQQKI;`gt-wHLXLur9o zcI;}Ow$L?h=NHczy*ujwE}06sYM7C7>=3;qw=Yr3#d(!rgoHwHMfO=9m{fFo8eEpo z8zcq_;w%p^22E+F-_>M$EVv@?O&IHdl4lfaGaMxHlOj`o^}?ou;h#_GxJK}5f|=^1C^Ej@?&oC1sbk@qDC($H?T#yB`1gV31EwFx1=|T{65*bclXQT ze;W0Ned2p0h@Aa3=wTJ@O4P>9^BaC)*_b)pqgsAP9=rOZM2irUVbjt)I0n2l|L$|c zMTCEOs)8EYUwJ3a@>Inz`Uu361RTFm+tAf#CJPZPtkfkJc&vq%9qZ8{g5lX;UI%yV z=g&jr%m`E6&gm5W>!p|EKlb`*)9?swRAgJ8+braQaKx)P+3sZhI=B!* zof=vLSL*)eV);mpu-Pha#rs9vTO_|Y1>y0;Aiu5@PZG(0bCxXpoz?4TJMLo{jq5PW zu#!Yx9@<|VM5K+@W+xkjLYFgv@SpoF=EO&-Q^%(p9&f;W+SuLJYEJo>GFb@6A>=e_ zA7jJQ3H{gnhgpfxw~?*G?kf4e^cCp>3e&qFm`{@A#k)JC`0?fL(3CJV+}DRFA!Bj} z7mbC^fyw>IGMlcZ_FQU#ASOMVK$f^G$A(Dt?W6g?C&85oHEj?Q#@p@HTm-wi+Z0P&;d*eB?nTn{@bz&yE4z=lwZS45u{}aA>9%5l*4MeF9+tu35%MLZBBD z2uaE)4ymHT(+7XXlmy4pWy@Wrjrxa>1XPHF^rzAG{-%jH_-c5vV-4*vJY}IdkJzX< zq>s!n;b0y1VYivPAl$N?N{LqUws{=p$&o$3{xlcM8bPf0)u<3L{tSfzgg_DPKI6B% zhkgsCPQI^yGW8nBzEyGHy=q!XuTni9IP%W4*AA#kte@AcV4I(&>1?rvCF9$RYZK#% zep@UP>Gu$ESkwE*C;q1aHvRCY6Yx0dDi!r@!p!Jv4jiXy8YAaFHlHLsqBwUV%ten( zBbc>c@^e`T?86X4d(Wdd=lAhACjGoEB4D1JpI>SYiDYK^fs5Ca`O)=KAZP$%i(Y}A z^&$@(_0Wogi7q?F(*bKqY_0{rK%31WJ)``T9F~vtPR*hgbAPQ#*Mk+bf}=wI+DTl8 z1%35L0l0vU0NKt9@0G`dyQK{ULCt(~fKY*Ih2cX(zif&PJ>75V4sxJtDOn>kBJ?07 zU+xc}hI(U`RSZk*Uh$=98zb*kVNUY3=-y8cP{|evlky({EN;|cAb)?j;t?Pg>gJdM zFg6KD(wh2u$pP|b!k-A$q{@>-|4xnp(wQ6V4DBSMG0ZTtu3L! zGF)LhQo> zsw+J3w^vTq_prr~*WG?wJA^I9!qW6p>rC#PS5=iH8W|F*Q%(Ew!w%eV3 z__y6OC}OQ$f*2on2RTGNC5^Y`r`&&-(HQ~cxdFG2pwvne^L6A~slGG`CDJZYEC+hm z51o#Kom&uCI!Ey1j-;1^5yZ%z^ueDSeyo_hGEmLM9oJT)hj^G}P6qDrkj0P~A|F3- z1F`?f?JYN=Al@OgI{%aT`#0}r|J@h=E3f!(u)_uH zH;!(2`F9+&TX?`1)RV`A)fa{so6Xzut?#fez6_B5MUYcHMcpor z?(z+;G^=3n{49U*+N;vG#cwFP+E1JPI~Vf5BW*l_lPBSvRD5@f^!a3AGxYh1bu@e5 zfQ34!N3YGVk)I3DJE2Uxoyv=k%qqzlEV;A)eyb0Hs4C1b0N##)keriw@^)A@^UJ;; z?Y~^x`cq0scOiTrO1Mw3r9>z2_!n`Zq@uALj+FrTu@@-->;oub8#+2PP&(NeGAsY% z4Ey`|ps^hy{uD;;&kevF4R;L$IGu2SHrl!u=0}#3BoPT78wYwaPfj z0r<}AKCTh_h0?$8q?SFui6>!TPPXBM3CO7P9Cb=M@9*BqB3#d^6)lGz)VMjjaxJ(X z4&bUaDoWgeYm~+fy?Xy%9CY?6^f^pMLxv>4k1taQvZ~Xqg#~D~YUsyaTy?b8dhMlr z?a_kt%440g-0+soyWyV7ZvTzF{}__++(b)ze_X3DKJwT~wyRM)WXVp91mIU!mKE{h z!+e$L6w0G1;<_{29qbPnV1k;x$C(KCKv?rQnRPcsjf z0Do=d8+iPS`tL)=WlKajP57EomvW_Gs$`KnE#&vCvdMm`aop8F+;{Z&uh-0=61Su$ zj+vg(K2wBG`XH=fwOcH#X0lkFyot;3n@+E|jplXH0k@y^d>&XHx%UsiC{z0Gr#Pq* zgccfdznj6;eU6V2ZP90b8{thr&|FL)kqIjHP9aMfV*!Z9fJ5w@1E}ka`Oh`cL=C>7 zp8Qa|y$S@iyLR%Z`UW^LIhL_pacev||C4cN0VUnT!wk9VBg*vvA}TE$C@DQy%bz~| z-9u!+h;^AVX=lITEEB<-+P@mtkGd~bpr;)WkjC3cbC{H7(n9Lkd&NKko*_=IS|nPh zH8g#k2xno~A#i;8j0JSTEODrVTGWY`<$G^`o(@N(7(vzeY~{%hIF9!eK-Y%Oc)&ZW zcmc=vlQl+W;h6~c`vIZtl6%I8aa^kB(0?At7)hpA?}Pk77u`q=(AbN&tjr|}3%;s& zsO;N%dKw?(^zG%gLMhT_hZjZ&h2k!cRz`U*;*vM?E309A&|1;3&&@p-25|(6fXXk< z8SQTQM}Ea3Zhg~G*h~I}5(;5t|4>?}EWI4;dOdV`>tcbPMxR3{lN9on2d=NtFU~g7 z5YEiPfwpz8-;Z$T_nw$R0`zCSx31y3CrrDD4pMR6M6XTD1VrL@Bt(Sra)oFhMv8%f z45e2XNUki+zwPQHqV|tO5s@Lb`ZM~;j7DXqH&?dJClt2(-kN0%gnw_j7?%w$7CnTj zRVw}x0wvKPoMfK^W;j1+fj0-ML0J@Tq#g%5%e8wXCd;hS<6Xy7R1BeDN zRVr=M=*O;{fcC6_F9Mv)bhK}%fWD07PPu<+pb9F@xM9o2Rix_2nzs2I3TyPR!f z@kl-j0C`Z={lL#MK4%{R089YnCEy9C7EsznA`|>`*LN!hZA%oJ4H$*S1_b<9Q3ONm zAqo<*e+$R}G6@-?cn|uw0AS<12O)~r=1ABlSjExO>c7RvC2YShiPpEcFRbc~hxy0Q z4)ku|SncM&ZF(iqMw~Xwd-c>eW5hfRVVEjEawd}rO;OfWQg-dTs#*hVnrv8}lWUb` z;BP9mI>ZQ3@ZQCLa6CmokroW?p5%PgJ+8f}G5F2Tdzcd-`W6fk^uS)q0px3yQ`x#- zrEBJm+q~hQN?$AExhjpiV{g7>5I}eRFj|$LUHBnXpd#D@pF95X1d_RbB!PF{cZRP^ zO$=9w3f#tT*ZP)71(K0(9o!k*281M~$qwYr%cu5_4y!%hc+~3{57E3cB>iC&%e=!F z=mq*w3mNtt-U@XH3Y54v#O1HzkQA3T+&PzPU3%_8U!Hk?3d01wxMSaO2X1jSx}uOR zxYNP&=GQXd*u9*vm$SK{OY@RpzDS(mx8wA)f8PAPByauP6Zp-c@No*FTk8XI;-$o6 zMZvA0Pz_c^BLT(it5uI6b)4oR3$nDm(3W0?z3aMS*OkMau+W_&3tRvxkxRjhI;(>(yG9VG9o+}M8z{veuy3?&RJ^12eI1+->R ziJrOecg-Wk!(YHrckh>*CHu7&%n3t1BOMZd`H8jdKT{(_9yYsn&v!DcG+DyjJ~w#JU7k7yf$ohh zeV#Dvn1|I9XXyOI{?%?zIS1jb%S{LqxUoEO_eaQw6TP4>ih-;`Fo)n(^TwmWLAN_z z)S|7haJz8I1S$et=g9nk@AA&X3WTIs-o(`EylnBL{!?d!K}v?h$i?NNMLKV6Fi0z{ zaLzMjQT1v_(P{chFwOi^wT;kI)=6^M%;m}tkj_-*-RqHla1y_(ds2#%l_OgJyG01n zY0={4(d*7Bk~&mnITGSdSQoAreO;4U#yP*u-c$L6YK2(*8|NWDQHz&{3H*C%EFjE{ z;x<=+-|X*>yIp`oaYvD=pbLI-4}E5>|L&W*YP{WngH z;YwYeOJ~2u3zRpnm%J;Nq#}B_n`-9FFb;jXIy~A3YE2*RYE#Y;^LPmVKLI7Bf=s7z-(ZgM=jhMNvQ8iwT)o#$!oPp~frD#lEw~rgBW%Ai!YKX5bov#Xu8oJe*WKFy3&-N21gNe<~mI(+#4eSVc{^;g1*6 z=qXI50n061;%SuqC!DE0!-i4qt>7VR957Ta0Rt9DX|;zFhsBre`7bVcLuV_hj^)O< zKrc<-=5-0z$Z7io!Ar^z%ES=?ygRg$UH7q{ocmm`v)L?NNK{AX^OvxU{Uw!a^V-h= z1aIUE{wl#*)}3F%xMln&v+K^wdWgImvZo)6NWs_1?cpQap5W`-2!-ZNh~h=WbX1>L zG<;{YKvxE2ZwA+Vpn*%AaxKAYt~FzZQ$%%aQK@Hoqp*-;z6;B~#XsKeX9A-^7<2jD zxSU=O7rSm|=9xQOezCY{rPk^T?ZGM+410${O!eNMfsNErn~io9q=O`fhl#A0AarKm z#OglsvoDBAba&aDZy*7kt0~eAP(6&ZU<@v5Eh|~40wTOfie=`3_u8SdBWiogyC2Er zd^v0k*&bExe`M@1%B_=y>&Ys^wfrzyjnTgvm+*UKd&Xq$juL!OH_otg=F2j4YJ})Y z=1XnJp-;KkGNF>uCyox4r9gz}_<#aGqR-w|cWH`_NJeGU7*-%5PntT8We0&~p z3`y~>-{8;9(~jV!dju;e(PwySJVYqS4sa(rNHu#sWAaczqK1OQzbZhABn8<9vNXN-u&z`xi7)x4ZlWNUJ^+mPqKni6>gN@X>%>ofEcW+q?>>4oXdtv zB5_U$RIzp1ncn(v2te^JEK5B zNR*Z!W_(>ghdvcLjYf*qE^hrR%BMARDKgTu5hVj~Dgo$11&mNZmkHh%gZ{(oDe#}hG_3td=i@|^th z7_#F5Da`P%3iw+$I+^ngR#@w@w5YLyGoP1!pn2`&KYw?}phrHkV?oF6T;v_`b|x>C zH0t{}w6O+AeI%KuHt@df^hnt*0`1aAob~)w9VM!RG;6Z%7K?#D5N8NaYr;(y;)hCB z%rO3g;M^?RSlH2oeT{!XIHCQcY!xpxc3Z(OYu!~lb{X}W`$-lky)_-Tv-65)N*uh%0-aV#jgxm<^@9nuk|lOb~+w~E5CkWXOE)9_fc zc6GZXU6hl-8e0%yC))_4gcm(P%T8JK(of8b)lTAZi76V$|z zI622S^ga=LqCDtCs0%!#&a=i?t1za=aiM2*b^4Qn6E}=$D}OP=*y81U)#ZF?`Zwiq zAj5=uD1Q%ZZ8rBtYIWL|sZ^c~TyCVf(Di)dov-VuT~QqCnn#~D$&uru&m4hHjIeIg z+mtZh4r|w(l@{F>P<(`wBIujDB^VdJjrwPhfhH?i2=}~}6ECI>GsDAqY0Dbo*e!2BgcF90S-J)Pp0dZ51z7_if~5e$WN;##0CmDsWxDgNbFGr zhY>?z{Th2^0~R zSi2x)XUc_62KC{74%~PHW>~3jo5|BB_tJE_HPUzyMKfSVfbT7Q`lPl#!8RPm1ZQa;c|jX~UP2``=`lIES{?@3(rFZ(4B@+P$EzBQZWU`8_9 z-lfuuHE-EDTM11{_Tm0H`Q>l3WGt`Y|Au@DO#ldDLtUoA>Gpk6rU=rM+_OH6F@35N z>w9}WGgwmn=$p z`(>0(_g3Kd#WmMGw0d=h2nTdYW}!!~Zcr=ld}i*s-3S8(0C-6NzXXOR0o1qXsE2uh za2Nm=2T=?n^8yjYzx%j2|L*@U4?@5oKr|W{1TjaVAVU=Y>?1z-cb|gne|4~V8^9qI X7R67fj*~D1K)jUX)!)|3g2Vp@Is6>c diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/common/media/runProject.png b/src/vs/workbench/contrib/welcome/gettingStarted/common/media/runProject.png deleted file mode 100644 index f90629c6945c526ffc7d99b31a8e8414739a3941..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65764 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 diff --git a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.contribution.ts b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.contribution.ts index e82f45492d..2bffb7f214 100644 --- a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.contribution.ts +++ b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.contribution.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; @@ -8,18 +8,40 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } fr import { Registry } from 'vs/platform/registry/common/platform'; import { WelcomePageContribution, WelcomePageAction, WelcomeInputSerializer } from 'sql/workbench/contrib/welcome/page/browser/welcomePage'; // {{SQL CARBON EDIT}} use our welcome page import { - WelcomeInputSerializer as WelcomeInputSerializer2, DEFAULT_STARTUP_EDITOR_CONFIG, + WelcomeInputSerializer as WelcomeInputSerializer2, WelcomePageContribution as WelcomePageContribution2, WelcomePageAction as WelcomePageAction2 } from 'vs/workbench/contrib/welcome/page/browser/welcomePage'; // {{SQL CARBON EDIT}} use our welcome page import { IWorkbenchActionRegistry, Extensions as ActionExtensions, CATEGORIES } from 'vs/workbench/common/actions'; import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IEditorInputFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; // {{SQL CARBON EDIT}} - use our welcome page Registry.as(ConfigurationExtensions.Configuration) - .registerConfiguration(DEFAULT_STARTUP_EDITOR_CONFIG); + .registerConfiguration({ + ...workbenchConfigurationNodeBase, + 'properties': { + 'workbench.startupEditor': { + 'scope': ConfigurationScope.RESOURCE, + 'type': 'string', + 'enum': ['none', 'welcomePageWithTour', 'welcomePage', 'readme', 'newUntitledFile', 'welcomePageInEmptyWorkbench'], // {{SQL CARBON EDIT}} Add our own welcomePageWithTour + 'enumDescriptions': [ + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.none' }, "Start without an editor."), + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePageWithTour' }, "Open the welcome page with Getting Started Tour (default)"), // {{SQL CARBON EDIT}} Add our own welcomePageWithTour + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePage' }, "Open the legacy Welcome page."), + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.readme' }, "Open the README when opening a folder that contains one, fallback to 'welcomePage' otherwise. Note: This is only observed as a global ccnfiguration, it will be ignored if set in a workspace or folder configuration."), + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.newUntitledFile' }, "Open a new untitled file (only applies when opening an empty window)."), + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePageInEmptyWorkbench' }, "Open the legacy Welcome page when opening an empty workbench."), + // localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.gettingStarted' }, "Open the new Welcome Page with content to aid in getting started with VS Code and extensions."), // {{SQL CARBON EDIT}} We don't use the VS Code gettingStarted experience + // localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.gettingStartedInEmptyWorkbench' }, "When opening an empty workbench, open the new Welcome Page with content to aid in getting started with VS Code and extensions.") // {{SQL CARBON EDIT}} We don't use the VS Code gettingStarted experience + ], + 'default': 'welcomePageWithTour', // {{SQL CARBON EDIT}} Remove gettingStarted page + 'description': localize('workbench.startupEditor', "Controls which editor is shown at startup, if none are restored from the previous session.") + }, + } + }); // {{SQL CARBON EDIT}} - determine whether to show preview or stable welcome page class WelcomeContributions { diff --git a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts index 0d99f68ea2..94836aa9e6 100644 --- a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts +++ b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts @@ -10,7 +10,7 @@ import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/c import * as arrays from 'vs/base/common/arrays'; import { WalkThroughInput } from 'vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { onUnexpectedError, isPromiseCanceledError } from 'vs/base/common/errors'; import { IWindowOpenable } from 'vs/platform/windows/common/windows'; @@ -32,7 +32,7 @@ import { registerThemingParticipant } from 'vs/platform/theme/common/themeServic import { focusBorder, textLinkForeground, textLinkActiveForeground, foreground, descriptionForeground, contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils'; import { IExtensionsViewPaneContainer, IExtensionsWorkbenchService, VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; -import { IEditorInputSerializer, EditorInput } from 'vs/workbench/common/editor'; +import { IEditorInputSerializer } from 'vs/workbench/common/editor'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { TimeoutTimer } from 'vs/base/common/async'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -49,63 +49,15 @@ import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/la import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { GettingStartedInput, gettingStartedInputTypeId } from 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedInput'; import { welcomeButtonBackground, welcomeButtonHoverBackground, welcomePageBackground } from 'vs/workbench/contrib/welcome/page/browser/welcomePageColors'; -import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationNode, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; -import { ILogService } from 'vs/platform/log/common/log'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -export const DEFAULT_STARTUP_EDITOR_CONFIG: IConfigurationNode = { - ...workbenchConfigurationNodeBase, - 'properties': { - 'workbench.startupEditor': { - 'scope': ConfigurationScope.RESOURCE, - 'type': 'string', - 'enum': ['none', 'welcomePageWithTour', 'welcomePage', 'readme', 'newUntitledFile', 'welcomePageInEmptyWorkbench'], // {{SQL CARBON EDIT}} Add our own welcomePageWithTour - 'enumDescriptions': [ - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.none' }, "Start without an editor."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePageWithTur' }, "Open the welcome page with Getting Started Tour (default)"), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePage' }, "Open the Welcome page."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.readme' }, "Open the README when opening a folder that contains one, fallback to 'welcomePage' otherwise."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.newUntitledFile' }, "Open a new untitled file (only applies when opening an empty window)."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePageInEmptyWorkbench' }, "Open the Welcome page when opening an empty workbench.") - // localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.gettingStarted' }, "Open the Getting Started page.")] // {{SQL CARBON EDIT}} We don't use the VS Code gettingStarted experience - ], - 'default': 'welcomePageWithTour', - 'description': localize('workbench.startupEditor', "Controls which editor is shown at startup, if none are restored from the previous session.") - }, - } -}; - -export const EXPERIMENTAL_GETTING_STARTED_STARTUP_EDITOR_CONFIG: IConfigurationNode = { - ...workbenchConfigurationNodeBase, - 'properties': { - 'workbench.startupEditor': { - 'scope': ConfigurationScope.RESOURCE, - 'type': 'string', - 'enum': ['none', 'welcomePage', 'readme', 'newUntitledFile', 'welcomePageInEmptyWorkbench', 'gettingStarted'], - 'enumDescriptions': [...[ - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.none' }, "Start without an editor."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePage' }, "Open the Welcome page."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.readme' }, "Open the README when opening a folder that contains one, fallback to 'welcomePage' otherwise."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.newUntitledFile' }, "Open a new untitled file (only applies when opening an empty window)."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePageInEmptyWorkbench' }, "Open the Welcome page when opening an empty workbench."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.gettingStarted' }, "Open the Getting Started page.")] - ], - 'default': 'gettingStarted', - 'description': localize('workbench.startupEditor', "Controls which editor is shown at startup, if none are restored from the previous session.") - }, - } -}; - const configurationKey = 'workbench.startupEditor'; const oldConfigurationKey = 'workbench.welcome.enabled'; const telemetryFrom = 'welcomePage'; export class WelcomePageContribution implements IWorkbenchContribution { private experimentManagementComplete: Promise; - private tasExperimentService: ITASExperimentService | undefined; constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -117,11 +69,7 @@ export class WelcomePageContribution implements IWorkbenchContribution { @ILifecycleService private readonly lifecycleService: ILifecycleService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @ICommandService private readonly commandService: ICommandService, - @ITelemetryService private readonly telemetryService: ITelemetryService, - @ILogService private readonly logService: ILogService, - @optional(ITASExperimentService) tasExperimentService: ITASExperimentService, ) { - this.tasExperimentService = tasExperimentService; // Run immediately to minimize time spent waiting for exp service. this.experimentManagementComplete = this.manageDefaultValuesForGettingStartedExperiment().catch(onUnexpectedError); @@ -134,43 +82,6 @@ export class WelcomePageContribution implements IWorkbenchContribution { if (this.lifecycleService.startupKind === StartupKind.ReloadedWindow || config.value !== config.defaultValue) { return; } - - if (this.configurationService.getValue('workbench.gettingStartedTreatmentOverride')) { - await new Promise(resolve => setTimeout(resolve, 1000)); - Registry.as(ConfigurationExtensions.Configuration).deregisterConfigurations([DEFAULT_STARTUP_EDITOR_CONFIG]); - Registry.as(ConfigurationExtensions.Configuration).registerConfiguration(EXPERIMENTAL_GETTING_STARTED_STARTUP_EDITOR_CONFIG); - } - - let someValueReturned = false; - type GettingStartedTreatmentData = { value: string; }; - type GettingStartedTreatmentClassification = { value: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; }; - - const tasUseGettingStartedAsDefault = this.tasExperimentService?.getTreatment('StartupGettingStarted') - .then(result => { - this.logService.trace('StartupGettingStarted:', result); - this.telemetryService.publicLog2('gettingStartedTreatmentValue', { value: '' + !!result }); - someValueReturned = true; - return result; - }) - .catch(error => { - this.logService.error('Recieved error when consulting experiment service for getting started experiment', error); - this.telemetryService.publicLog2('gettingStartedTreatmentValue', { value: 'err' }); - someValueReturned = true; - return false; - }); - - const fallback = new Promise(c => setTimeout(() => c(false), 2000)).then( - () => { - if (!someValueReturned) { this.logService.trace('Unable to read getting started treatment data in time, falling back to welcome'); } - someValueReturned = true; - } - ); - - const useGettingStartedAsDefault = !!await Promise.race([tasUseGettingStartedAsDefault, fallback]); - if (useGettingStartedAsDefault) { - Registry.as(ConfigurationExtensions.Configuration).deregisterConfigurations([DEFAULT_STARTUP_EDITOR_CONFIG]); - Registry.as(ConfigurationExtensions.Configuration).registerConfiguration(EXPERIMENTAL_GETTING_STARTED_STARTUP_EDITOR_CONFIG); - } } private async run() { @@ -226,7 +137,7 @@ export class WelcomePageContribution implements IWorkbenchContribution { await this.experimentManagementComplete; const startupEditorSetting = this.configurationService.getValue(configurationKey); - const startupEditorTypeID = startupEditorSetting === 'gettingStarted' ? gettingStartedInputTypeId : welcomeInputTypeId; + const startupEditorTypeID = (startupEditorSetting === 'gettingStarted' || startupEditorSetting === 'gettingStartedInEmptyWorkbench') ? gettingStartedInputTypeId : welcomeInputTypeId; const editor = this.editorService.activeEditor; // Ensure that the welcome editor won't get opened more than once @@ -250,8 +161,15 @@ function isWelcomePageEnabled(configurationService: IConfigurationService, conte return welcomeEnabled.value; } } + if (startupEditor.value === 'readme' && startupEditor.userValue !== 'readme') { + console.error('Warning: `workbench.startupEditor: readme` setting ignored due to being set somewhere other than user settings'); + } // {{SQL CARBON EDIT}} - add welcomePageWithTour - return startupEditor.value === 'welcomePageWithTour' || startupEditor.value === 'welcomePage' || startupEditor.value === 'gettingStarted' || startupEditor.value === 'readme' || startupEditor.value === 'welcomePageInEmptyWorkbench' && contextService.getWorkbenchState() === WorkbenchState.EMPTY; + return startupEditor.value === 'welcomePageWithTour' + || startupEditor.value === 'welcomePage' + || startupEditor.value === 'gettingStarted' + || startupEditor.userValue === 'readme' + || (contextService.getWorkbenchState() === WorkbenchState.EMPTY && (startupEditor.value === 'welcomePageInEmptyWorkbench' || startupEditor.value === 'gettingStartedInEmptyWorkbench')); } export class WelcomePageAction extends Action { @@ -752,10 +670,10 @@ export class WelcomeInputSerializer implements IEditorInputSerializer { } public serialize(editorInput: EditorInput): string { - return '{}'; + return ''; } - public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): WalkThroughInput { + public deserialize(instantiationService: IInstantiationService): WalkThroughInput { return instantiationService.createInstance(WelcomePage) .editorInput; } diff --git a/src/vs/workbench/contrib/welcome/telemetryOptOut/browser/telemetryOptOut.ts b/src/vs/workbench/contrib/welcome/telemetryOptOut/browser/telemetryOptOut.ts index 72c71cec57..94e5ff9ab9 100644 --- a/src/vs/workbench/contrib/welcome/telemetryOptOut/browser/telemetryOptOut.ts +++ b/src/vs/workbench/contrib/welcome/telemetryOptOut/browser/telemetryOptOut.ts @@ -67,6 +67,7 @@ export abstract class AbstractTelemetryOptOut implements IWorkbenchContribution } private showTelemetryOptOut(telemetryOptOutUrl: string): void { + // {{SQL CARBON EDIT}} const optOutNotice = localize('telemetryOptOut.optOutNotice', "Help improve Azure Data Studio by allowing Microsoft to collect usage data. Read our [privacy statement]({0}) and learn how to [opt out]({1}).", this.privacyUrl, this.productService.telemetryOptOutUrl); const optInNotice = localize('telemetryOptOut.optInNotice', "Help improve Azure Data Studio by allowing Microsoft to collect usage data. Read our [privacy statement]({0}) and learn how to [opt in]({1}).", this.privacyUrl, this.productService.telemetryOptOutUrl); diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/editorWalkThrough.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/editorWalkThrough.ts index e4693042b8..b11c629119 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/editorWalkThrough.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/editorWalkThrough.ts @@ -10,8 +10,9 @@ import { Action } from 'vs/base/common/actions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { WalkThroughInput, WalkThroughInputOptions } from 'vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput'; import { FileAccess, Schemas } from 'vs/base/common/network'; -import { IEditorInputSerializer, EditorInput } from 'vs/workbench/common/editor'; +import { IEditorInputSerializer } from 'vs/workbench/common/editor'; import { EditorOverride } from 'vs/platform/editor/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; const typeId = 'workbench.editors.walkThroughInput'; const inputOptions: WalkThroughInputOptions = { @@ -55,10 +56,10 @@ export class EditorWalkThroughInputSerializer implements IEditorInputSerializer } public serialize(editorInput: EditorInput): string { - return '{}'; + return ''; } - public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): WalkThroughInput { + public deserialize(instantiationService: IInstantiationService): WalkThroughInput { return instantiationService.createInstance(WalkThroughInput, inputOptions); } } diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts index 9e4dbdf845..73accd00af 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts @@ -3,7 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorInput, EditorModel } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { URI } from 'vs/base/common/uri'; import { DisposableStore, IReference } from 'vs/base/common/lifecycle'; import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -130,10 +131,7 @@ export class WalkThroughInput extends EditorInput { } if (otherInput instanceof WalkThroughInput) { - let otherResourceEditorInput = otherInput; - - // Compare by properties - return isEqual(otherResourceEditorInput.options.resource, this.options.resource); + return isEqual(otherInput.options.resource, this.options.resource); } return false; diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts index 847548e88d..b02fe4b316 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts @@ -10,7 +10,7 @@ import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import * as strings from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { EditorOptions, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WalkThroughInput } from 'vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput'; @@ -26,7 +26,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { Event } from 'vs/base/common/event'; import { isObject } from 'vs/base/common/types'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { IEditorOptions, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IEditorOptions as ICodeEditorOptions, EditorOption } from 'vs/editor/common/config/editorOptions'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { registerColor, focusBorder, textLinkForeground, textLinkActiveForeground, textPreformatForeground, contrastBorder, textBlockQuoteBackground, textBlockQuoteBorder } from 'vs/platform/theme/common/colorRegistry'; import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils'; @@ -39,6 +39,7 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { CancellationToken } from 'vs/base/common/cancellation'; import { domEvent } from 'vs/base/browser/event'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; export const WALK_THROUGH_FOCUS = new RawContextKey('interactivePlaygroundFocus', false); @@ -267,7 +268,7 @@ export class WalkThroughPart extends EditorPane { this.scrollbar.setScrollPosition({ scrollTop: scrollPosition.scrollTop + scrollDimensions.height }); } - override setInput(input: WalkThroughInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override setInput(input: WalkThroughInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { if (this.input instanceof WalkThroughInput) { this.saveTextEditorViewState(this.input); } @@ -420,7 +421,7 @@ export class WalkThroughPart extends EditorPane { }); } - private getEditorOptions(language: string): IEditorOptions { + private getEditorOptions(language: string): ICodeEditorOptions { const config = deepClone(this.configurationService.getValue('editor', { overrideIdentifier: language })); return { ...isObject(config) ? config : Object.create(null), diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index 4bdd5fa900..812bb995af 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -1,69 +1,189 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import 'vs/css!./workspaceTrustEditor'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; -import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { Severity } from 'vs/platform/notification/common/notification'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, WorkspaceTrustRequestOptions, workspaceTrustToString } from 'vs/platform/workspace/common/workspaceTrust'; +import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, workspaceTrustToString } from 'vs/platform/workspace/common/workspaceTrust'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Codicon } from 'vs/base/common/codicons'; import { ThemeColor } from 'vs/workbench/api/common/extHostTypes'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/common/statusbar'; import { IEditorRegistry, EditorDescriptor } from 'vs/workbench/browser/editor'; -import { WorkspaceTrustEditor } from 'vs/workbench/contrib/workspace/browser/workspaceTrustEditor'; +import { shieldIcon, WorkspaceTrustEditor } from 'vs/workbench/contrib/workspace/browser/workspaceTrustEditor'; import { WorkspaceTrustEditorInput } from 'vs/workbench/services/workspaces/browser/workspaceTrustEditorInput'; -import { isWorkspaceTrustEnabled, WorkspaceTrustContext, WORKSPACE_TRUST_ENABLED, WORKSPACE_TRUST_STARTUP_PROMPT } from 'vs/workbench/services/workspaces/common/workspaceTrust'; -import { EditorInput, IEditorInputSerializer, IEditorInputFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor'; +import { WorkspaceTrustContext, WORKSPACE_TRUST_EMPTY_WINDOW, WORKSPACE_TRUST_ENABLED, WORKSPACE_TRUST_STARTUP_PROMPT, WORKSPACE_TRUST_UNTRUSTED_FILES } from 'vs/workbench/services/workspaces/common/workspaceTrust'; +import { IEditorInputSerializer, IEditorInputFactoryRegistry, EditorExtensions, EditorResourceAccessor } from 'vs/workbench/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { isWeb } from 'vs/base/common/platform'; import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; import { dirname, resolve } from 'vs/base/common/path'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import product from 'vs/platform/product/common/product'; -import { MarkdownString } from 'vs/base/common/htmlContent'; +import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { Schemas } from 'vs/base/common/network'; import { STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND } from 'vs/workbench/common/theme'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { splitName } from 'vs/base/common/labels'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IBannerItem, IBannerService } from 'vs/workbench/services/banner/browser/bannerService'; +import { isVirtualWorkspace } from 'vs/platform/remote/common/remoteHosts'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; +const BANNER_RESTRICTED_MODE = 'workbench.banner.restrictedMode'; const STARTUP_PROMPT_SHOWN_KEY = 'workspace.trust.startupPrompt.shown'; - +const BANNER_RESTRICTED_MODE_DISMISSED_KEY = 'workbench.banner.restrictedMode.dismissed'; /* - * Trust Request UX Handler + * Trust Request via Service UX handler */ -export class WorkspaceTrustRequestHandler extends Disposable implements IWorkbenchContribution { +export class WorkspaceTrustRequestHandler extends Disposable implements IWorkbenchContribution { constructor( @IDialogService private readonly dialogService: IDialogService, @ICommandService private readonly commandService: ICommandService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, + @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService) { + super(); + + this.registerListeners(); + } + + private get useWorkspaceLanguage(): boolean { + return !isSingleFolderWorkspaceIdentifier(toWorkspaceIdentifier(this.workspaceContextService.getWorkspace())); + } + + private get modalTitle(): string { + return this.useWorkspaceLanguage ? + localize('workspaceTrust', "Do you trust the authors of the files in this workspace?") : + localize('folderTrust', "Do you trust the authors of the files in this folder?"); + } + + private async registerListeners(): Promise { + await this.workspaceTrustManagementService.workspaceResolved; + this._register(this.workspaceTrustRequestService.onDidInitiateWorkspaceTrustRequest(async requestOptions => { + // Message + const defaultMessage = localize('immediateTrustRequestMessage', "A feature you are trying to use may be a security risk if you do not trust the source of the files or folders you currently have open."); + const message = requestOptions?.message ?? defaultMessage; + + // Buttons + const buttons = requestOptions?.buttons ?? [ + { label: this.useWorkspaceLanguage ? localize('grantWorkspaceTrustButton', "Trust Workspace & Continue") : localize('grantFolderTrustButton', "Trust Folder & Continue"), type: 'ContinueWithTrust' }, + { label: localize('manageWorkspaceTrustButton', "Manage"), type: 'Manage' } + ]; + // Add Cancel button if not provided + if (!buttons.some(b => b.type === 'Cancel')) { + buttons.push({ label: localize('cancelWorkspaceTrustButton', "Cancel"), type: 'Cancel' }); + } + + // Dialog + const result = await this.dialogService.show( + Severity.Info, + this.modalTitle, + buttons.map(b => b.label), + { + cancelId: buttons.findIndex(b => b.type === 'Cancel'), + custom: { + icon: Codicon.shield, + markdownDetails: [ + { markdown: new MarkdownString(message) }, + { markdown: new MarkdownString(localize('immediateTrustRequestLearnMore', "If you don't trust the authors of these files, we do not recommend continuing as the files may be malicious. See [our docs](https://aka.ms/vscode-workspace-trust) to learn more.")) } + ] + } + } + ); + + // Dialog result + switch (buttons[result.choice].type) { + case 'ContinueWithTrust': + await this.workspaceTrustRequestService.completeRequest(true); + break; + case 'ContinueWithoutTrust': + await this.workspaceTrustRequestService.completeRequest(undefined); + break; + case 'Manage': + this.workspaceTrustRequestService.cancelRequest(); + await this.commandService.executeCommand(MANAGE_TRUST_COMMAND_ID); + break; + case 'Cancel': + this.workspaceTrustRequestService.cancelRequest(); + break; + } + })); + } +} + + +/* + * Trust UX and Startup Handler + */ +export class WorkspaceTrustUXHandler extends Disposable implements IWorkbenchContribution { + + private readonly entryId = `status.workspaceTrust.${this.workspaceContextService.getWorkspace().id}`; + + private readonly statusbarEntryAccessor: MutableDisposable; + + // try showing the banner only after some files have been opened + private showIndicatorsInEmptyWindow = false; + + constructor( + @IDialogService private readonly dialogService: IDialogService, + @IEditorService private readonly editorService: IEditorService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IStatusbarService private readonly statusbarService: IStatusbarService, @IStorageService private readonly storageService: IStorageService, @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, + @IBannerService private readonly bannerService: IBannerService, + @IHostService private readonly hostService: IHostService, ) { super(); - if (isWorkspaceTrustEnabled(configurationService)) { - this.registerListeners(); - this.showModalOnStart(); - } + this.statusbarEntryAccessor = this._register(new MutableDisposable()); + + (async () => { + + await this.workspaceTrustManagementService.workspaceTrustInitialized; + + if (this.workspaceTrustManagementService.workspaceTrustEnabled) { + this.registerListeners(); + this.createStatusbarEntry(); + + // Set empty workspace trust state + await this.setEmptyWorkspaceTrustState(); + + // Show modal dialog + if (this.hostService.hasFocus) { + this.showModalOnStart(); + } else { + const focusDisposable = this.hostService.onDidChangeFocus(focused => { + if (focused) { + focusDisposable.dispose(); + this.showModalOnStart(); + } + }); + } + } + })(); } private get startupPromptSetting(): 'always' | 'once' | 'never' { @@ -108,12 +228,13 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben switch (result.choice) { case 0: if (result.checkboxChecked) { - this.workspaceTrustManagementService.setParentFolderTrust(true); + await this.workspaceTrustManagementService.setParentFolderTrust(true); } else { - this.workspaceTrustRequestService.completeRequest(true); + await this.workspaceTrustRequestService.completeRequest(true); } break; case 1: + this.updateWorkbenchIndicators(false); this.workspaceTrustRequestService.cancelRequest(); break; } @@ -121,16 +242,36 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben this.storageService.store(STARTUP_PROMPT_SHOWN_KEY, true, StorageScope.WORKSPACE, StorageTarget.MACHINE); } - private showModalOnStart(): void { + private async showModalOnStart(): Promise { if (this.workspaceTrustManagementService.isWorkpaceTrusted()) { + this.updateWorkbenchIndicators(true); + return; + } + + // Don't show modal prompt if workspace trust cannot be changed + if (!(this.workspaceTrustManagementService.canSetWorkspaceTrust())) { + return; + } + + // Don't show modal prompt for virtual workspaces by default + if (isVirtualWorkspace(this.workspaceContextService.getWorkspace())) { + this.updateWorkbenchIndicators(false); + return; + } + + // Don't show modal prompt for empty workspaces by default + if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY) { + this.updateWorkbenchIndicators(false); return; } if (this.startupPromptSetting === 'never') { + this.updateWorkbenchIndicators(false); return; } if (this.startupPromptSetting === 'once' && this.storageService.getBoolean(STARTUP_PROMPT_SHOWN_KEY, StorageScope.WORKSPACE, false)) { + this.updateWorkbenchIndicators(false); return; } @@ -138,7 +279,9 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben const workspaceIdentifier = toWorkspaceIdentifier(this.workspaceContextService.getWorkspace())!; const isSingleFolderWorkspace = isSingleFolderWorkspaceIdentifier(workspaceIdentifier); if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && workspaceIdentifier.uri.scheme === Schemas.file) { - checkboxText = localize('checkboxString', "I trust the authors of all files in the parent folder"); + const { parentPath } = splitName(workspaceIdentifier.uri.fsPath); + const { name } = splitName(parentPath); + checkboxText = localize('checkboxString', "Trust the authors of all files in the parent folder '{0}'", name); } // Show Workspace Trust Start Dialog @@ -148,72 +291,212 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben { label: localize('dontTrustOption', "No, I don't trust the authors"), sublabel: isSingleFolderWorkspace ? localize('dontTrustFolderOptionDescription', "Browse folder in restricted mode") : localize('dontTrustWorkspaceOptionDescription', "Browse workspace in restricted mode") }, [ !isSingleFolderWorkspace ? - localize('workspaceStartupTrustDetails', "{0} provides advanced editing features that may automatically execute files in this workspace.", product.nameShort) : - localize('folderStartupTrustDetails', "{0} provides advanced editing features that may automatically execute files in this folder.", product.nameShort), + localize('workspaceStartupTrustDetails', "{0} provides features that may automatically execute files in this workspace.", product.nameShort) : + localize('folderStartupTrustDetails', "{0} provides features that may automatically execute files in this folder.", product.nameShort), localize('startupTrustRequestLearnMore', "If you don't trust the authors of these files, we recommend to continue in restricted mode as the files may be malicious. See [our docs](https://aka.ms/vscode-workspace-trust) to learn more.") ], checkboxText ); } - private registerListeners(): void { - this._register(this.workspaceTrustRequestService.onDidInitiateWorkspaceTrustRequest(async requestOptions => { - if (requestOptions.modal) { - // Message - const defaultMessage = localize('immediateTrustRequestMessage', "A feature you are trying to use may be a security risk if you do not trust the source of the files or folders you currently have open."); - const message = requestOptions.message ?? defaultMessage; + private createStatusbarEntry(): void { + const entry = this.getStatusbarEntry(this.workspaceTrustManagementService.isWorkpaceTrusted()); + this.statusbarEntryAccessor.value = this.statusbarService.addEntry(entry, this.entryId, StatusbarAlignment.LEFT, 0.99 * Number.MAX_VALUE /* Right of remote indicator */); + this.statusbarService.updateEntryVisibility(this.entryId, false); + } - // Buttons - const buttons = requestOptions.buttons ?? [ - { label: this.useWorkspaceLanguage ? localize('grantWorkspaceTrustButton', "Trust Workspace & Continue") : localize('grantFolderTrustButton', "Trust Folder & Continue"), type: 'ContinueWithTrust' }, - { label: localize('manageWorkspaceTrustButton', "Manage"), type: 'Manage' } - ]; - // Add Cancel button if not provided - if (!buttons.some(b => b.type === 'Cancel')) { - buttons.push({ label: localize('cancelWorkspaceTrustButton', "Cancel"), type: 'Cancel' }); + private getBannerItem(restrictedMode: boolean): IBannerItem | undefined { + + const dismissedRestricted = this.storageService.getBoolean(BANNER_RESTRICTED_MODE_DISMISSED_KEY, StorageScope.WORKSPACE, false); + + // info has been dismissed + if (dismissedRestricted) { + return undefined; + } + + const actions = + [ + { + label: localize('restrictedModeBannerManage', "Manage"), + href: 'command:' + MANAGE_TRUST_COMMAND_ID + }, + { + label: localize('restrictedModeBannerLearnMore', "Learn More"), + href: 'https://aka.ms/vscode-workspace-trust' } + ]; - // Dialog - const result = await this.dialogService.show( - Severity.Info, - this.modalTitle, - buttons.map(b => b.label), - { - cancelId: buttons.findIndex(b => b.type === 'Cancel'), - custom: { - icon: Codicon.shield, - markdownDetails: [ - { markdown: new MarkdownString(message) }, - { markdown: new MarkdownString(localize('immediateTrustRequestLearnMore', "If you don't trust the authors of these files, we do not recommend continuing as the files may be malicious. See [our docs](https://aka.ms/vscode-workspace-trust) to learn more.")) } - ] - } - } - ); - - // Dialog result - switch (buttons[result.choice].type) { - case 'ContinueWithTrust': - this.workspaceTrustRequestService.completeRequest(true); - break; - case 'ContinueWithoutTrust': - this.workspaceTrustRequestService.completeRequest(undefined); - break; - case 'Manage': - this.workspaceTrustRequestService.cancelRequest(); - await this.commandService.executeCommand('workbench.trust.manage'); - break; - case 'Cancel': - this.workspaceTrustRequestService.cancelRequest(); - break; + return { + id: BANNER_RESTRICTED_MODE, + icon: shieldIcon, + ariaLabel: this.getBannerItemAriaLabels(), + message: this.getBannerItemMessages(), + actions, + onClose: () => { + if (restrictedMode) { + this.storageService.store(BANNER_RESTRICTED_MODE_DISMISSED_KEY, true, StorageScope.WORKSPACE, StorageTarget.MACHINE); } } - })); + }; + } + + private getBannerItemAriaLabels(): string { + switch (this.workspaceContextService.getWorkbenchState()) { + case WorkbenchState.EMPTY: + return localize('restrictedModeBannerAriaLabelWindow', "Restricted Mode is intended for safe code browsing. Trust this window to enable all features. Use navigation keys to access banner actions."); + case WorkbenchState.FOLDER: + return localize('restrictedModeBannerAriaLabelFolder', "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features. Use navigation keys to access banner actions."); + case WorkbenchState.WORKSPACE: + return localize('restrictedModeBannerAriaLabelWorkspace', "Restricted Mode is intended for safe code browsing. Trust this workspace to enable all features. Use navigation keys to access banner actions."); + } + } + + private getBannerItemMessages(): string { + switch (this.workspaceContextService.getWorkbenchState()) { + case WorkbenchState.EMPTY: + return localize('restrictedModeBannerMessageWindow', "Restricted Mode is intended for safe code browsing. Trust this window to enable all features."); + case WorkbenchState.FOLDER: + return localize('restrictedModeBannerMessageFolder', "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features."); + case WorkbenchState.WORKSPACE: + return localize('restrictedModeBannerMessageWorkspace', "Restricted Mode is intended for safe code browsing. Trust this workspace to enable all features."); + } + } + + private getStatusbarEntry(trusted: boolean): IStatusbarEntry { + const text = workspaceTrustToString(trusted); + const backgroundColor = new ThemeColor(STATUS_BAR_PROMINENT_ITEM_BACKGROUND); + const color = new ThemeColor(STATUS_BAR_PROMINENT_ITEM_FOREGROUND); + + let ariaLabel = ''; + let toolTip: IMarkdownString | string | undefined; + switch (this.workspaceContextService.getWorkbenchState()) { + case WorkbenchState.EMPTY: { + ariaLabel = trusted ? localize('status.ariaTrustedWindow', "This window is trusted.") : + localize('status.ariaUntrustedWindow', "Restricted Mode: Some features are disabled because this window is not trusted."); + toolTip = trusted ? ariaLabel : { + value: localize( + { key: 'status.tooltipUntrustedWindow2', comment: ['[abc]({n}) are links. Only translate `features are disabled` and `window is not trusted`. Do not change brackets and parentheses or {n}'] }, + "Running in Restricted Mode\n\nSome [features are disabled]({0}) because this [window is not trusted]({1}).", + `command:${LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID}`, + `command:${MANAGE_TRUST_COMMAND_ID}` + ), + isTrusted: true, + supportThemeIcons: true + }; + break; + } + case WorkbenchState.FOLDER: { + ariaLabel = trusted ? localize('status.ariaTrustedFolder', "This folder is trusted.") : + localize('status.ariaUntrustedFolder', "Restricted Mode: Some features are disabled because this folder is not trusted."); + toolTip = trusted ? ariaLabel : { + value: localize( + { key: 'status.tooltipUntrustedFolder2', comment: ['[abc]({n}) are links. Only translate `features are disabled` and `folder is not trusted`. Do not change brackets and parentheses or {n}'] }, + "Running in Restricted Mode\n\nSome [features are disabled]({0}) because this [folder is not trusted]({1}).", + `command:${LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID}`, + `command:${MANAGE_TRUST_COMMAND_ID}` + ), + isTrusted: true, + supportThemeIcons: true + }; + break; + } + case WorkbenchState.WORKSPACE: { + ariaLabel = trusted ? localize('status.ariaTrustedWorkspace', "This workspace is trusted.") : + localize('status.ariaUntrustedWorkspace', "Restricted Mode: Some features are disabled because this workspace is not trusted."); + toolTip = trusted ? ariaLabel : { + value: localize( + { key: 'status.tooltipUntrustedWorkspace2', comment: ['[abc]({n}) are links. Only translate `features are disabled` and `workspace is not trusted`. Do not change brackets and parentheses or {n}'] }, + "Running in Restricted Mode\n\nSome [features are disabled]({0}) because this [workspace is not trusted]({1}).", + `command:${LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID}`, + `command:${MANAGE_TRUST_COMMAND_ID}` + ), + isTrusted: true, + supportThemeIcons: true + }; + break; + } + } + + return { + name: localize('status.WorkspaceTrust', "Workspace Trust"), + text: trusted ? `$(shield)` : `$(shield) ${text}`, + ariaLabel: ariaLabel, + tooltip: toolTip, + command: MANAGE_TRUST_COMMAND_ID, + backgroundColor, + color + }; + } + + private async setEmptyWorkspaceTrustState(): Promise { + if (this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY) { + return; + } + + // Open files + const openFiles = this.editorService.editors.map(editor => EditorResourceAccessor.getCanonicalUri(editor, { filterByScheme: Schemas.file })).filter(uri => !!uri); + + if (openFiles.length) { + this.showIndicatorsInEmptyWindow = true; + + // If all open files are trusted, transition to a trusted workspace + const openFilesTrustInfo = await Promise.all(openFiles.map(uri => this.workspaceTrustManagementService.getUriTrustInfo(uri!))); + + if (openFilesTrustInfo.map(info => info.trusted).every(trusted => trusted)) { + this.workspaceTrustManagementService.setWorkspaceTrust(true); + } + } else { + // No open files, use the setting to set workspace trust state + const disposable = this._register(this.editorService.onDidActiveEditorChange(() => { + const editor = this.editorService.activeEditor; + if (editor && !!EditorResourceAccessor.getCanonicalUri(editor, { filterByScheme: Schemas.file })) { + this.showIndicatorsInEmptyWindow = true; + this.updateWorkbenchIndicators(this.workspaceTrustManagementService.isWorkpaceTrusted()); + disposable.dispose(); + } + })); + // TODO: Consider moving the check into setWorkspaceTrust() + // TODO: Consider moving this into calculateWorkspaceTrust() + if (this.workspaceTrustManagementService.canSetWorkspaceTrust() && + this.configurationService.getValue(WORKSPACE_TRUST_EMPTY_WINDOW)) { + this.workspaceTrustManagementService.setWorkspaceTrust(true); + } + } + } + + private updateStatusbarEntry(trusted: boolean): void { + this.statusbarEntryAccessor.value?.update(this.getStatusbarEntry(trusted)); + this.updateStatusbarEntryVisibility(trusted); + } + + private updateStatusbarEntryVisibility(trusted: boolean): void { + this.statusbarService.updateEntryVisibility(this.entryId, !trusted); + } + + private updateWorkbenchIndicators(trusted: boolean): void { + const isEmptyWorkspace = this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY; + const bannerItem = this.getBannerItem(!trusted); + + if (!isEmptyWorkspace || this.showIndicatorsInEmptyWindow) { + this.updateStatusbarEntry(trusted); + + if (bannerItem) { + if (!trusted) { + this.bannerService.show(bannerItem); + } else { + this.bannerService.hide(BANNER_RESTRICTED_MODE); + } + } + } + } + + private registerListeners(): void { this._register(this.workspaceContextService.onWillChangeWorkspaceFolders(e => { if (e.fromCache) { return; } - if (!isWorkspaceTrustEnabled(this.configurationService)) { + if (!this.workspaceTrustManagementService.workspaceTrustEnabled) { return; } const trusted = this.workspaceTrustManagementService.isWorkpaceTrusted(); @@ -221,8 +504,9 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben return e.join(new Promise(async resolve => { // Workspace is trusted and there are added/changed folders if (trusted && (e.changes.added.length || e.changes.changed.length)) { - const addedFoldersTrustInfo = e.changes.added.map(folder => this.workspaceTrustManagementService.getFolderTrustInfo(folder.uri)); - if (!addedFoldersTrustInfo.map(i => i.trusted).every(trusted => trusted)) { + const addedFoldersTrustInfo = await Promise.all(e.changes.added.map(folder => this.workspaceTrustManagementService.getUriTrustInfo(folder.uri))); + + if (!addedFoldersTrustInfo.map(info => info.trusted).every(trusted => trusted)) { const result = await this.dialogService.show( Severity.Info, localize('addWorkspaceFolderMessage', "Do you trust the authors of the files in this folder?"), @@ -235,7 +519,7 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben ); // Mark added/changed folders as trusted - this.workspaceTrustManagementService.setFoldersTrust(addedFoldersTrustInfo.map(i => i.uri), result.choice === 0); + await this.workspaceTrustManagementService.setUrisTrust(addedFoldersTrustInfo.map(i => i.uri), result.choice === 0); resolve(); } @@ -244,75 +528,15 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben resolve(); })); })); + + this._register(this.workspaceTrustManagementService.onDidChangeTrust(trusted => { + this.updateWorkbenchIndicators(trusted); + })); } } Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(WorkspaceTrustRequestHandler, LifecyclePhase.Ready); - -/* - * Status Bar Entry - */ -class WorkspaceTrustStatusbarItem extends Disposable implements IWorkbenchContribution { - private readonly entryId = `status.workspaceTrust.${this.workspaceService.getWorkspace().id}`; - private readonly statusBarEntryAccessor: MutableDisposable; - private pendingRequestContextKey = WorkspaceTrustContext.PendingRequest.key; - private contextKeys = new Set([this.pendingRequestContextKey]); - - constructor( - @IConfigurationService configurationService: IConfigurationService, - @IStatusbarService private readonly statusbarService: IStatusbarService, - @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, - @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, - @IContextKeyService private readonly contextKeyService: IContextKeyService - ) { - super(); - - this.statusBarEntryAccessor = this._register(new MutableDisposable()); - - if (isWorkspaceTrustEnabled(configurationService)) { - const entry = this.getStatusbarEntry(this.workspaceTrustManagementService.isWorkpaceTrusted()); - this.statusBarEntryAccessor.value = this.statusbarService.addEntry(entry, this.entryId, localize('status.WorkspaceTrust', "Workspace Trust"), StatusbarAlignment.LEFT, 0.99 * Number.MAX_VALUE /* Right of remote indicator */); - this._register(this.workspaceTrustManagementService.onDidChangeTrust(trusted => this.updateStatusbarEntry(trusted))); - this._register(this.contextKeyService.onDidChangeContext((contextChange) => { - if (contextChange.affectsSome(this.contextKeys)) { - this.updateVisibility(this.workspaceTrustManagementService.isWorkpaceTrusted()); - } - })); - - this.updateVisibility(this.workspaceTrustManagementService.isWorkpaceTrusted()); - } - } - - private getStatusbarEntry(trusted: boolean): IStatusbarEntry { - const text = workspaceTrustToString(trusted); - const backgroundColor = new ThemeColor(STATUS_BAR_PROMINENT_ITEM_BACKGROUND); - const color = new ThemeColor(STATUS_BAR_PROMINENT_ITEM_FOREGROUND); - - return { - text: trusted ? `$(shield)` : `$(shield) ${text}`, - ariaLabel: trusted ? localize('status.ariaTrusted', "This workspace is trusted.") : localize('status.ariaUntrusted', "Restricted Mode: Some features are disabled because this workspace is not trusted."), - tooltip: trusted ? localize('status.tooltipTrusted', "This workspace is trusted.") : localize('status.tooltipUntrusted', "Some features are disabled because this workspace is not trusted."), - command: 'workbench.trust.manage', - backgroundColor, - color - }; - } - - private updateVisibility(trusted: boolean): void { - const pendingRequest = this.contextKeyService.getContextKeyValue(this.pendingRequestContextKey) === true; - this.statusbarService.updateEntryVisibility(this.entryId, !trusted || pendingRequest); - } - - private updateStatusbarEntry(trusted: boolean): void { - this.statusBarEntryAccessor.value?.update(this.getStatusbarEntry(trusted)); - this.updateVisibility(trusted); - } -} - -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution( - WorkspaceTrustStatusbarItem, - LifecyclePhase.Starting -); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(WorkspaceTrustUXHandler, LifecyclePhase.Restored); /** * Trusted Workspace GUI Editor @@ -324,10 +548,10 @@ class WorkspaceTrustEditorInputSerializer implements IEditorInputSerializer { } serialize(input: WorkspaceTrustEditorInput): string { - return '{}'; + return ''; } - deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): WorkspaceTrustEditorInput { + deserialize(instantiationService: IInstantiationService): WorkspaceTrustEditorInput { return instantiationService.createInstance(WorkspaceTrustEditorInput); } } @@ -350,11 +574,13 @@ Registry.as(EditorExtensions.Editors).registerEditor( * Actions */ +const MANAGE_TRUST_COMMAND_ID = 'workbench.trust.manage'; + // Manage Workspace Trust registerAction2(class extends Action2 { constructor() { super({ - id: 'workbench.trust.manage', + id: MANAGE_TRUST_COMMAND_ID, title: { original: 'Manage Workspace Trust', value: localize('manageWorkspaceTrust', "Manage Workspace Trust") @@ -364,7 +590,7 @@ registerAction2(class extends Action2 { id: MenuId.GlobalActivity, group: '6_workspace_trust', order: 40, - when: ContextKeyExpr.and(IsWebContext.negate(), ContextKeyExpr.equals(`config.${WORKSPACE_TRUST_ENABLED}`, true), WorkspaceTrustContext.PendingRequest.negate()) + when: ContextKeyExpr.and(WorkspaceTrustContext.IsEnabled, IsWebContext.negate(), ContextKeyExpr.equals(`config.${WORKSPACE_TRUST_ENABLED}`, true)) }, }); } @@ -380,16 +606,6 @@ registerAction2(class extends Action2 { } }); -MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { - command: { - id: 'workbench.trust.manage', - title: localize('manageWorkspaceTrustPending', "Manage Workspace Trust (1)"), - }, - group: '6_workspace_trust', - order: 40, - when: ContextKeyExpr.and(IsWebContext.negate(), ContextKeyExpr.equals(`config.${WORKSPACE_TRUST_ENABLED}`, true), WorkspaceTrustContext.PendingRequest) -}); - /* * Configuration */ @@ -403,10 +619,10 @@ Registry.as(ConfigurationExtensions.Configuration) properties: { [WORKSPACE_TRUST_ENABLED]: { type: 'boolean', - default: false, + default: true, included: !isWeb, description: localize('workspace.trust.description', "Controls whether or not workspace trust is enabled within VS Code."), - scope: ConfigurationScope.APPLICATION + scope: ConfigurationScope.APPLICATION, }, [WORKSPACE_TRUST_STARTUP_PROMPT]: { type: 'string', @@ -420,6 +636,26 @@ Registry.as(ConfigurationExtensions.Configuration) localize('workspace.trust.startupPrompt.once', "Ask for trust the first time an untrusted workspace is opened."), localize('workspace.trust.startupPrompt.never', "Do not ask for trust when an untrusted workspace is opened."), ] + }, + [WORKSPACE_TRUST_UNTRUSTED_FILES]: { + type: 'string', + default: 'prompt', + included: !isWeb, + markdownDescription: localize('workspace.trust.untrustedFiles.description', "Controls how to handle opening untrusted files in a trusted workspace. This setting also applies to opening files in an empty window which is trusted via `#{0}#`.", WORKSPACE_TRUST_EMPTY_WINDOW), + scope: ConfigurationScope.APPLICATION, + enum: ['prompt', 'open', 'newWindow'], + enumDescriptions: [ + localize('workspace.trust.untrustedFiles.prompt', "Ask how to handle untrusted files for each workspace. Once untrusted files are introduced to a trusted workspace, you will not be prompted again."), + localize('workspace.trust.untrustedFiles.open', "Always allow untrusted files to be introduced to a trusted workspace without prompting."), + localize('workspace.trust.untrustedFiles.newWindow', "Always open untrusted files in a separate window in restricted mode without prompting."), + ] + }, + [WORKSPACE_TRUST_EMPTY_WINDOW]: { + type: 'boolean', + default: true, + included: !isWeb, + markdownDescription: localize('workspace.trust.emptyWindow.description', "Controls whether or not the empty window is trusted by default within VS Code. When used with `#{0}#`, you can enable the full functionality of VS Code without prompting in an empty window.", WORKSPACE_TRUST_UNTRUSTED_FILES), + scope: ConfigurationScope.APPLICATION } } }); @@ -429,7 +665,6 @@ Registry.as(ConfigurationExtensions.Configuration) */ class WorkspaceTrustTelemetryContribution extends Disposable implements IWorkbenchContribution { constructor( - @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionService private readonly extensionService: IExtensionService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @@ -439,13 +674,13 @@ class WorkspaceTrustTelemetryContribution extends Disposable implements IWorkben super(); this._register(this.workspaceTrustManagementService.onDidChangeTrust(isTrusted => this.logWorkspaceTrustChangeEvent(isTrusted))); - this._register(this.workspaceTrustRequestService.onDidInitiateWorkspaceTrustRequest(options => this.logWorkspaceTrustRequest(options))); + this._register(this.workspaceTrustRequestService.onDidInitiateWorkspaceTrustRequest(_ => this.logWorkspaceTrustRequest())); this.logInitialWorkspaceTrustInfo(); } private logInitialWorkspaceTrustInfo(): void { - if (!isWorkspaceTrustEnabled(this.configurationService)) { + if (!this.workspaceTrustManagementService.workspaceTrustEnabled) { return; } @@ -458,12 +693,12 @@ class WorkspaceTrustTelemetryContribution extends Disposable implements IWorkben }; this.telemetryService.publicLog2('workspaceTrustFolderCounts', { - trustedFoldersCount: this.workspaceTrustManagementService.getTrustedFolders().length, + trustedFoldersCount: this.workspaceTrustManagementService.getTrustedUris().length, }); } - private logWorkspaceTrustChangeEvent(isTrusted: boolean): void { - if (!isWorkspaceTrustEnabled(this.configurationService)) { + private async logWorkspaceTrustChangeEvent(isTrusted: boolean): Promise { + if (!this.workspaceTrustManagementService.workspaceTrustEnabled) { return; } @@ -508,7 +743,7 @@ class WorkspaceTrustTelemetryContribution extends Disposable implements IWorkben }; for (const folder of this.workspaceContextService.getWorkspace().folders) { - const { trusted, uri } = this.workspaceTrustManagementService.getFolderTrustInfo(folder.uri); + const { trusted, uri } = await this.workspaceTrustManagementService.getUriTrustInfo(folder.uri); if (!trusted) { continue; } @@ -522,25 +757,22 @@ class WorkspaceTrustTelemetryContribution extends Disposable implements IWorkben } } - private async logWorkspaceTrustRequest(options: WorkspaceTrustRequestOptions): Promise { - if (!isWorkspaceTrustEnabled(this.configurationService)) { + private async logWorkspaceTrustRequest(): Promise { + if (!this.workspaceTrustManagementService.workspaceTrustEnabled) { return; } type WorkspaceTrustRequestedEventClassification = { - modal: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; workspaceId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; extensions: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; }; type WorkspaceTrustRequestedEvent = { - modal: boolean, workspaceId: string, extensions: string[] }; this.telemetryService.publicLog2('workspaceTrustRequested', { - modal: options.modal, workspaceId: this.workspaceContextService.getWorkspace().id, extensions: (await this.extensionService.getExtensions()).filter(ext => !!ext.capabilities?.untrustedWorkspaces).map(ext => ext.identifier.value) }); diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustColors.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustColors.ts deleted file mode 100644 index 9260d6261b..0000000000 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustColors.ts +++ /dev/null @@ -1,14 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { localize } from 'vs/nls'; -import { editorErrorForeground, registerColor } from 'vs/platform/theme/common/colorRegistry'; -import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugColors'; -import { welcomePageTileBackground } from 'vs/workbench/contrib/welcome/page/browser/welcomePageColors'; - -export const trustedForegroundColor = registerColor('workspaceTrust.trustedForegound', { dark: debugIconStartForeground, light: debugIconStartForeground, hc: debugIconStartForeground }, localize('workspaceTrustTrustedColor', 'Color used when indicating a workspace is trusted.')); -export const untrustedForegroundColor = registerColor('workspaceTrust.untrustedForeground', { dark: editorErrorForeground, light: editorErrorForeground, hc: editorErrorForeground }, localize('workspaceTrustUntrustedColor', 'Color used when indicating a workspace is not trusted.')); - -export const trustEditorTileBackgroundColor = registerColor('workspaceTrust.tileBackground', { dark: welcomePageTileBackground, light: welcomePageTileBackground, hc: welcomePageTileBackground }, localize('workspaceTrust.tileBackground', 'Background color for the tiles on the Workspace Trust page.')); diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css index de08b35453..5cf878a54f 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css @@ -1,18 +1,18 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ .monaco-icon-label.file-icon.workspacetrusteditor-name-file-icon.ext-file-icon.tab-label::before { font-family: 'codicon'; content: '\eb53'; + background-image: none; + font-size: 150%; } .workspace-trust-editor { max-width: 1000px; padding-top: 11px; - padding-left: 15px; - padding-right: 15px; margin: auto; height: calc(100% - 11px); } @@ -42,7 +42,7 @@ } .workspace-trust-editor .workspace-trust-header .workspace-trust-title .workspace-trust-title-icon { - color: var(--workspace-trust-selected-state-color) !important; + color: var(--workspace-trust-selected-color) !important; } .workspace-trust-editor .workspace-trust-header .workspace-trust-description { @@ -74,21 +74,23 @@ user-select: text; display: flex; flex-direction: row; + flex-flow: wrap; justify-content: space-evenly; } .workspace-trust-editor .workspace-trust-features .workspace-trust-limitations { min-height: 315px; + border: 1px solid var(--workspace-trust-unselected-color); + margin: 4px 4px; + display: flex; + flex-direction: column; + padding: 10px 40px; } -.workspace-trust-editor.trusted .workspace-trust-features .workspace-trust-limitations.trusted { - border-width: 2px; - border-color: var(--workspace-trust-selected-state-color) !important; -} - +.workspace-trust-editor.trusted .workspace-trust-features .workspace-trust-limitations.trusted, .workspace-trust-editor.untrusted .workspace-trust-features .workspace-trust-limitations.untrusted { border-width: 2px; - border-color: var(--workspace-trust-selected-state-color) !important; + border-color: var(--workspace-trust-selected-color) !important; } .workspace-trust-editor .workspace-trust-features .workspace-trust-limitations ul { @@ -107,12 +109,12 @@ } .workspace-trust-editor .workspace-trust-features .workspace-trust-limitations.trusted .list-item-icon { - color: var(--workspace-trust-trusted-color) !important; + color: var(--workspace-trust-check-color) !important; font-size: 18px; } .workspace-trust-editor .workspace-trust-features .workspace-trust-limitations.untrusted .list-item-icon { - color: var(--workspace-trust-untrusted-color) !important; + color: var(--workspace-trust-x-color) !important; font-size: 20px; } @@ -141,14 +143,10 @@ display: none; } -.workspace-trust-editor.trusted .workspace-trust-features .workspace-trust-limitations.trusted .workspace-trust-limitations-header .workspace-trust-limitations-title .workspace-trust-title-icon { - display: unset; - color: var(--workspace-trust-selected-state-color) !important; -} - +.workspace-trust-editor.trusted .workspace-trust-features .workspace-trust-limitations.trusted .workspace-trust-limitations-header .workspace-trust-limitations-title .workspace-trust-title-icon, .workspace-trust-editor.untrusted .workspace-trust-features .workspace-trust-limitations.untrusted .workspace-trust-limitations-header .workspace-trust-limitations-title .workspace-trust-title-icon { display: unset; - color: var(--workspace-trust-selected-state-color) !important; + color: var(--workspace-trust-selected-color) !important; } .workspace-trust-editor .workspace-trust-features .workspace-trust-untrusted-description { @@ -179,61 +177,23 @@ padding: 5px 10px; overflow: hidden; text-overflow: ellipsis; - margin: 4px 5px; /* allows button focus outline to be visible */ outline-offset: 2px !important; } -.workspace-trust-editor .workspace-trust-features .workspace-trust-buttons-row .workspace-trust-buttons .monaco-button-dropdown { - padding: 0 2px; -} - -.workspace-trust-editor .workspace-trust-features .workspace-trust-buttons-row .workspace-trust-buttons .monaco-button-dropdown .monaco-button { - margin-left: 1px; - margin-right: 1px; +.workspace-trust-editor .workspace-trust-features .workspace-trust-buttons-row .workspace-trust-buttons > .monaco-button, +.workspace-trust-editor .workspace-trust-features .workspace-trust-buttons-row .workspace-trust-buttons > .monaco-button-dropdown { + margin: 4px 5px; /* allows button focus outline to be visible */ } .workspace-trust-editor .workspace-trust-features .workspace-trust-buttons-row .workspace-trust-buttons .monaco-button-dropdown .monaco-dropdown-button { - padding: 5px 0px; + padding: 5px; } .workspace-trust-limitations { width: 50%; max-width: 350px; -} - -/** Settings */ -.workspace-trust-editor .workspace-trust-settings .workspace-trust-section-title { - padding: 14px; -} - -.workspace-trust-editor .settings-editor .settings-body { - margin-top: 0; -} - -.workspace-trust-editor .settings-editor .settings-body .settings-tree-container .shadow.top { - left: initial; - margin-left: initial; - max-width: initial; -} - -.workspace-trust-editor .settings-editor .settings-body .settings-tree-container .monaco-list-rows { - background: unset; -} - -.workspace-trust-editor .settings-editor .settings-body .settings-tree-container .monaco-list-row .monaco-tl-contents { - padding-left: 0; - padding-right: 0; -} - -.workspace-trust-editor .settings-editor .settings-body .settings-tree-container .monaco-list-row .monaco-tl-contents .setting-list-edit-row > .setting-list-valueInput { - width: 100%; - max-width: 100%; -} - -.workspace-trust-editor .settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-list-object-key, -.workspace-trust-editor .settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-list-object-input-key { - margin-left: 4px; - min-width: 20%; + min-width: 250px; + flex: 1; } .workspace-trust-intro-dialog { @@ -255,3 +215,66 @@ max-height: 32px; padding-right: 10px; } + +.workspace-trust-editor .workspace-trust-settings { + padding: 20px 14px; +} + +.workspace-trust-editor .workspace-trust-settings .workspace-trusted-folders-title { + font-weight: 600; +} + +.workspace-trust-editor .monaco-table-tr .monaco-table-td .path:not(.input-mode) .monaco-inputbox, +.workspace-trust-editor .monaco-table-tr .monaco-table-td .path.input-mode .path-label { + display: none; +} + +.workspace-trust-editor .monaco-table-tr .monaco-table-td .current-workspace-parent .path-label, +.workspace-trust-editor .monaco-table-tr .monaco-table-td .current-workspace-parent .host-label { + font-weight: bold; + font-style: italic; +} + + +.workspace-trust-editor .monaco-table-tr .monaco-table-td .path .monaco-inputbox input { + padding-left: 5px; +} + +.workspace-trust-editor .monaco-table-th, +.workspace-trust-editor .monaco-table-td { + padding-left: 5px; +} + +.workspace-trust-editor .workspace-trust-settings .monaco-action-bar .action-item > .codicon { + display: flex; + align-items: center; + justify-content: center; + color: inherit; +} + +.workspace-trust-editor .workspace-trust-settings .monaco-table-tr .monaco-table-td { + align-items: center; + display: flex; + overflow: hidden; +} + +.workspace-trust-editor .workspace-trust-settings .monaco-table-tr .monaco-table-td .path { + width: 100%; +} + +.workspace-trust-editor .workspace-trust-settings .monaco-table-tr .monaco-table-td .monaco-button { + height: 18px; + padding-left: 8px; + padding-right: 8px; +} + +.workspace-trust-editor .workspace-trust-settings .monaco-table-tr .monaco-table-td .actions .monaco-action-bar { + display: none; + flex: 1; +} + +.workspace-trust-editor .workspace-trust-settings .monaco-list-row.selected .monaco-table-tr .monaco-table-td .actions .monaco-action-bar, +.workspace-trust-editor .workspace-trust-settings .monaco-table .monaco-list-row.focused .monaco-table-tr .monaco-table-td .actions .monaco-action-bar, +.workspace-trust-editor .workspace-trust-settings .monaco-list-row:hover .monaco-table-tr .monaco-table-td .actions .monaco-action-bar { + display: flex; +} diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index 553260531d..dc74b194a9 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -1,58 +1,532 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, append, clearNode, Dimension, EventHelper } from 'vs/base/browser/dom'; +import { $, addDisposableListener, addStandardDisposableListener, append, clearNode, Dimension, EventHelper, EventType } from 'vs/base/browser/dom'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { ButtonBar } from 'vs/base/browser/ui/button/button'; +import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; -import { Action } from 'vs/base/common/actions'; +import { ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; +import { Action, IAction } from 'vs/base/common/actions'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Codicon, registerCodicon } from 'vs/base/common/codicons'; -import { Color, RGBA } from 'vs/base/common/color'; import { debounce } from 'vs/base/common/decorators'; -import { Iterable } from 'vs/base/common/iterator'; +import { Emitter, Event } from 'vs/base/common/event'; +import { KeyCode } from 'vs/base/common/keyCodes'; import { splitName } from 'vs/base/common/labels'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { parseLinkedText } from 'vs/base/common/linkedText'; import { Schemas } from 'vs/base/common/network'; -import { isEqual } from 'vs/base/common/resources'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; -import { isArray } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { ExtensionUntrustedWorkpaceSupportType } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IPromptChoiceWithMenu, Severity } from 'vs/platform/notification/common/notification'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { WorkbenchTable } from 'vs/platform/list/browser/listService'; +import { IPromptChoiceWithMenu } from 'vs/platform/notification/common/notification'; import { Link } from 'vs/platform/opener/browser/link'; import product from 'vs/platform/product/common/product'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { isVirtualWorkspace } from 'vs/platform/remote/common/remoteHosts'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { foreground } from 'vs/platform/theme/common/colorRegistry'; -import { attachButtonStyler, attachLinkStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; -import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { buttonBackground, buttonSecondaryBackground, editorErrorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { attachButtonStyler, attachInputBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; +import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { ChoiceAction } from 'vs/workbench/common/notifications'; -import { ACTIVITY_BAR_BADGE_BACKGROUND } from 'vs/workbench/common/theme'; -import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugColors'; +import { IExtensionsWorkbenchService, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { getInstalledExtensions, IExtensionStatus } from 'vs/workbench/contrib/extensions/common/extensionsUtils'; -import { trustedForegroundColor, untrustedForegroundColor } from 'vs/workbench/contrib/workspace/browser/workspaceTrustColors'; -import { IWorkspaceTrustSettingChangeEvent, WorkspaceTrustSettingArrayRenderer, WorkspaceTrustTree, WorkspaceTrustTreeModel } from 'vs/workbench/contrib/workspace/browser/workspaceTrustTree'; -import { filterSettingsRequireWorkspaceTrust, IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; +import { settingsEditIcon, settingsRemoveIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; +import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { WorkspaceTrustEditorInput } from 'vs/workbench/services/workspaces/browser/workspaceTrustEditorInput'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; -const shieldIcon = registerCodicon('workspace-trust-icon', Codicon.shield); +export const shieldIcon = registerCodicon('workspace-trust-icon', Codicon.shield); const checkListIcon = registerCodicon('workspace-trusted-check-icon', Codicon.check); const xListIcon = registerCodicon('workspace-trusted-x-icon', Codicon.x); +const enum TrustedUriItemType { + Existing = 1, + Add = 2 +} + +interface ITrustedUriItem { + entryType: TrustedUriItemType; + parentOfWorkspaceItem: boolean; + uri: URI; +} + +class WorkspaceTrustedUrisTable extends Disposable { + private readonly _onDidAcceptEdit: Emitter = this._register(new Emitter()); + readonly onDidAcceptEdit: Event = this._onDidAcceptEdit.event; + + private readonly _onDidRejectEdit: Emitter = this._register(new Emitter()); + readonly onDidRejectEdit: Event = this._onDidRejectEdit.event; + + private _onEdit: Emitter = this._register(new Emitter()); + readonly onEdit: Event = this._onEdit.event; + + private _onDelete: Emitter = this._register(new Emitter()); + readonly onDelete: Event = this._onDelete.event; + + private readonly table: WorkbenchTable; + + constructor( + private readonly container: HTMLElement, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, + @IUriIdentityService private readonly uriService: IUriIdentityService, + @IFileDialogService private readonly fileDialogService: IFileDialogService + ) { + super(); + + this.table = this.instantiationService.createInstance( + WorkbenchTable, + 'WorkspaceTrust', + this.container, + new TrustedUriTableVirtualDelegate(), + [ + { + label: localize('hostColumnLabel', "Host"), + tooltip: '', + weight: 1, + templateId: TrustedUriHostColumnRenderer.TEMPLATE_ID, + project(row: ITrustedUriItem): ITrustedUriItem { return row; } + }, + { + label: localize('pathColumnLabel', "Path"), + tooltip: '', + weight: 9, + templateId: TrustedUriPathColumnRenderer.TEMPLATE_ID, + project(row: ITrustedUriItem): ITrustedUriItem { return row; } + }, + { + label: '', + tooltip: '', + weight: 0, + minimumWidth: 55, + maximumWidth: 55, + templateId: TrustedUriActionsColumnRenderer.TEMPLATE_ID, + project(row: ITrustedUriItem): ITrustedUriItem { return row; } + }, + ], + [ + this.instantiationService.createInstance(TrustedUriHostColumnRenderer, this), + this.instantiationService.createInstance(TrustedUriPathColumnRenderer, this), + this.instantiationService.createInstance(TrustedUriActionsColumnRenderer, this), + ], + { + horizontalScrolling: false, + alwaysConsumeMouseWheel: false, + openOnSingleClick: false, + } + ) as WorkbenchTable; + + this._register(this.table.onDidOpen(item => { + // default prevented when input box is double clicked #125052 + if (item && item.element && !item.browserEvent?.defaultPrevented) { + this.edit(item.element); + } + })); + + this._register(this.workspaceTrustManagementService.onDidChangeTrustedFolders(() => { + this.updateTable(); + })); + } + + private getIndexOfTrustedUriEntry(item: ITrustedUriItem): number { + const index = this.trustedUriEntries.indexOf(item); + if (index === -1) { + for (let i = 0; i < this.trustedUriEntries.length; i++) { + if (this.trustedUriEntries[i].entryType !== item.entryType) { + continue; + } + + if (item.entryType === TrustedUriItemType.Add || this.trustedUriEntries[i].uri === item.uri) { + return i; + } + } + } + + return index; + } + + private selectTrustedUriEntry(item: ITrustedUriItem, focus: boolean = true): void { + const index = this.getIndexOfTrustedUriEntry(item); + if (index !== -1) { + if (focus) { + this.table.domFocus(); + this.table.setFocus([index]); + } + this.table.setSelection([index]); + } + } + + private get currentWorkspaceUri(): URI { + return this.workspaceService.getWorkspace().folders[0]?.uri || URI.file('/'); + } + + private get trustedUriEntries(): ITrustedUriItem[] { + const currentWorkspace = this.workspaceService.getWorkspace(); + const currentWorkspaceUris = currentWorkspace.folders.map(folder => folder.uri); + if (currentWorkspace.configuration) { + currentWorkspaceUris.push(currentWorkspace.configuration); + } + + const entries = this.workspaceTrustManagementService.getTrustedUris().map(uri => { + + let relatedToCurrentWorkspace = false; + for (const workspaceUri of currentWorkspaceUris) { + relatedToCurrentWorkspace = relatedToCurrentWorkspace || this.uriService.extUri.isEqualOrParent(workspaceUri, uri); + } + + return { + uri, + entryType: TrustedUriItemType.Existing, + parentOfWorkspaceItem: relatedToCurrentWorkspace + }; + }); + entries.push({ uri: this.currentWorkspaceUri, entryType: TrustedUriItemType.Add, parentOfWorkspaceItem: false }); + return entries; + } + + layout(): void { + this.table.layout((this.trustedUriEntries.length * TrustedUriTableVirtualDelegate.ROW_HEIGHT) + TrustedUriTableVirtualDelegate.HEADER_ROW_HEIGHT, undefined); + } + + updateTable(): void { + this.table.splice(0, Number.POSITIVE_INFINITY, this.trustedUriEntries); + this.layout(); + } + + acceptEdit(item: ITrustedUriItem, uri: URI) { + const trustedFolders = this.workspaceTrustManagementService.getTrustedUris(); + const index = this.getIndexOfTrustedUriEntry(item); + + if (index >= trustedFolders.length) { + trustedFolders.push(uri); + } else { + trustedFolders[index] = uri; + } + + this.workspaceTrustManagementService.setTrustedUris(trustedFolders); + this._onDidAcceptEdit.fire(item); + } + + rejectEdit(item: ITrustedUriItem) { + this._onDidRejectEdit.fire(item); + } + + async delete(item: ITrustedUriItem) { + await this.workspaceTrustManagementService.setUrisTrust([item.uri], false); + this._onDelete.fire(item); + } + + async edit(item: ITrustedUriItem) { + const canUseOpenDialog = item.uri.scheme === Schemas.file || + (item.uri.scheme === this.currentWorkspaceUri.scheme && this.uriService.extUri.isEqualAuthority(this.currentWorkspaceUri.authority, item.uri.authority)); + if (canUseOpenDialog) { + const uri = await this.fileDialogService.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + defaultUri: item.uri, + openLabel: localize('trustUri', "Trust Folder"), + title: localize('selectTrustedUri', "Select Folder To Trust") + }); + + if (uri) { + this.acceptEdit(item, uri[0]); + } else { + this.rejectEdit(item); + } + } else { + this.selectTrustedUriEntry(item); + this._onEdit.fire(item); + } + } +} + +class TrustedUriTableVirtualDelegate implements ITableVirtualDelegate { + static readonly HEADER_ROW_HEIGHT = 30; + static readonly ROW_HEIGHT = 24; + readonly headerRowHeight = TrustedUriTableVirtualDelegate.HEADER_ROW_HEIGHT; + getHeight(item: ITrustedUriItem) { + return TrustedUriTableVirtualDelegate.ROW_HEIGHT; + } +} + +interface IActionsColumnTemplateData { + readonly actionBar: ActionBar; +} + +class TrustedUriActionsColumnRenderer implements ITableRenderer { + + static readonly TEMPLATE_ID = 'actions'; + + readonly templateId: string = TrustedUriActionsColumnRenderer.TEMPLATE_ID; + + constructor(private readonly table: WorkspaceTrustedUrisTable) { } + + renderTemplate(container: HTMLElement): IActionsColumnTemplateData { + const element = container.appendChild($('.actions')); + const actionBar = new ActionBar(element, { animated: false }); + return { actionBar }; + } + + renderElement(item: ITrustedUriItem, index: number, templateData: IActionsColumnTemplateData, height: number | undefined): void { + templateData.actionBar.clear(); + + if (item.entryType !== TrustedUriItemType.Add) { + const actions: IAction[] = []; + actions.push(this.createEditAction(item)); + actions.push(this.createDeleteAction(item)); + templateData.actionBar.push(actions, { icon: true }); + } + } + + private createEditAction(item: ITrustedUriItem): IAction { + return { + class: ThemeIcon.asClassName(settingsEditIcon), + enabled: true, + id: 'editTrustedUri', + tooltip: localize('editTrustedUri', "Change Path"), + run: () => { + this.table.edit(item); + } + }; + } + + private createDeleteAction(item: ITrustedUriItem): IAction { + return { + class: ThemeIcon.asClassName(settingsRemoveIcon), + enabled: true, + id: 'deleteTrustedUri', + tooltip: localize('deleteTrustedUri', "Delete Path"), + run: async () => { + await this.table.delete(item); + } + }; + } + + disposeTemplate(templateData: IActionsColumnTemplateData): void { + templateData.actionBar.dispose(); + } + +} + +interface ITrustedUriPathColumnTemplateData { + element: HTMLElement; + pathLabel: HTMLElement; + pathInput: InputBox; + renderDisposables: DisposableStore; + disposables: DisposableStore; +} + +class TrustedUriPathColumnRenderer implements ITableRenderer { + static readonly TEMPLATE_ID = 'path'; + + readonly templateId: string = TrustedUriPathColumnRenderer.TEMPLATE_ID; + + constructor( + private readonly table: WorkspaceTrustedUrisTable, + @IContextViewService private readonly contextViewService: IContextViewService, + @IThemeService private readonly themeService: IThemeService, + ) { + } + + renderTemplate(container: HTMLElement): ITrustedUriPathColumnTemplateData { + const element = container.appendChild($('.path')); + const pathLabel = element.appendChild($('div.path-label')); + + const pathInput = new InputBox(element, this.contextViewService); + + const disposables = new DisposableStore(); + disposables.add(attachInputBoxStyler(pathInput, this.themeService)); + + const renderDisposables = disposables.add(new DisposableStore()); + + return { + element, + pathLabel, + pathInput, + disposables, + renderDisposables + }; + } + + renderElement(item: ITrustedUriItem, index: number, templateData: ITrustedUriPathColumnTemplateData, height: number | undefined): void { + templateData.renderDisposables.clear(); + + templateData.renderDisposables.add(this.table.onEdit(async (e) => { + if (item === e) { + templateData.element.classList.add('input-mode'); + templateData.pathInput.focus(); + templateData.pathInput.select(); + templateData.element.parentElement!.style.paddingLeft = '0px'; + } + })); + + // stop double click action from re-rendering the element on the table #125052 + templateData.renderDisposables.add(addDisposableListener(templateData.pathInput.element, EventType.DBLCLICK, e => { + EventHelper.stop(e); + })); + + + const hideInputBox = () => { + templateData.element.classList.remove('input-mode'); + templateData.element.parentElement!.style.paddingLeft = '5px'; + }; + + const accept = () => { + hideInputBox(); + const uri = item.uri.with({ path: templateData.pathInput.value }); + templateData.pathLabel.innerText = templateData.pathInput.value; + + if (uri) { + this.table.acceptEdit(item, uri); + } + }; + + const reject = () => { + hideInputBox(); + templateData.pathInput.value = stringValue; + this.table.rejectEdit(item); + }; + + templateData.renderDisposables.add(addStandardDisposableListener(templateData.pathInput.inputElement, EventType.KEY_DOWN, e => { + let handled = false; + if (e.equals(KeyCode.Enter)) { + accept(); + handled = true; + } else if (e.equals(KeyCode.Escape)) { + reject(); + handled = true; + } + + if (handled) { + e.preventDefault(); + e.stopPropagation(); + } + })); + templateData.renderDisposables.add((addDisposableListener(templateData.pathInput.inputElement, EventType.BLUR, () => { + reject(); + }))); + + const stringValue = item.uri.scheme === Schemas.file ? URI.revive(item.uri).fsPath : item.uri.path; + templateData.pathInput.value = stringValue; + templateData.pathLabel.innerText = stringValue; + templateData.element.classList.toggle('current-workspace-parent', item.parentOfWorkspaceItem); + + templateData.pathLabel.style.display = item.entryType === TrustedUriItemType.Add ? 'none' : ''; + } + + disposeTemplate(templateData: ITrustedUriPathColumnTemplateData): void { + templateData.disposables.dispose(); + templateData.renderDisposables.dispose(); + } + +} + + +interface ITrustedUriHostColumnTemplateData { + element: HTMLElement; + hostContainer: HTMLElement; + buttonBarContainer: HTMLElement; + disposables: DisposableStore; + renderDisposables: DisposableStore; +} + +class TrustedUriHostColumnRenderer implements ITableRenderer { + static readonly TEMPLATE_ID = 'host'; + + readonly templateId: string = TrustedUriHostColumnRenderer.TEMPLATE_ID; + + constructor( + private readonly table: WorkspaceTrustedUrisTable, + @ILabelService private readonly labelService: ILabelService, + @IThemeService private readonly themeService: IThemeService, + ) { } + + renderTemplate(container: HTMLElement): ITrustedUriHostColumnTemplateData { + const disposables = new DisposableStore(); + const renderDisposables = disposables.add(new DisposableStore()); + + const element = container.appendChild($('.host')); + const hostContainer = element.appendChild($('div.host-label')); + const buttonBarContainer = element.appendChild($('div.button-bar')); + + return { + element, + hostContainer, + buttonBarContainer, + disposables, + renderDisposables + }; + } + + renderElement(item: ITrustedUriItem, index: number, templateData: ITrustedUriHostColumnTemplateData, height: number | undefined): void { + templateData.renderDisposables.clear(); + templateData.renderDisposables.add({ dispose: () => { clearNode(templateData.buttonBarContainer); } }); + + templateData.hostContainer.innerText = item.uri.authority ? this.labelService.getHostLabel(item.uri.scheme, item.uri.authority) : localize('localAuthority', "Local"); + templateData.element.classList.toggle('current-workspace-parent', item.parentOfWorkspaceItem); + + if (item.entryType === TrustedUriItemType.Add) { + templateData.hostContainer.style.display = 'none'; + templateData.buttonBarContainer.style.display = ''; + + const buttonBar = templateData.renderDisposables.add(new ButtonBar(templateData.buttonBarContainer)); + const addButton = templateData.renderDisposables.add(buttonBar.addButton({ title: localize('addButton', "Add Folder") })); + addButton.label = localize('addButton', "Add Folder"); + + templateData.renderDisposables.add(attachButtonStyler(addButton, this.themeService)); + + templateData.renderDisposables.add(addButton.onDidClick(() => { + this.table.edit(item); + })); + + templateData.renderDisposables.add(this.table.onEdit(e => { + if (item === e) { + templateData.hostContainer.style.display = ''; + templateData.buttonBarContainer.style.display = 'none'; + } + })); + + templateData.renderDisposables.add(this.table.onDidRejectEdit(e => { + if (item === e) { + templateData.hostContainer.style.display = 'none'; + templateData.buttonBarContainer.style.display = ''; + } + })); + } else { + templateData.hostContainer.style.display = ''; + templateData.buttonBarContainer.style.display = 'none'; + } + } + + disposeTemplate(templateData: ITrustedUriHostColumnTemplateData): void { + templateData.disposables.dispose(); + } + +} + export class WorkspaceTrustEditor extends EditorPane { static readonly ID: string = 'workbench.editor.workspaceTrust'; private rootElement!: HTMLElement; @@ -71,9 +545,7 @@ export class WorkspaceTrustEditor extends EditorPane { // Settings Section private configurationContainer!: HTMLElement; - private trustSettingsTree!: WorkspaceTrustTree; - private workspaceTrustSettingsTreeModel!: WorkspaceTrustTreeModel; - + private workpaceTrustedUrisTable!: WorkspaceTrustedUrisTable; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -84,20 +556,20 @@ export class WorkspaceTrustEditor extends EditorPane { @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextMenuService private readonly contextMenuService: IContextMenuService, - @IDialogService private readonly dialogService: IDialogService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IWorkbenchConfigurationService private readonly configurationService: IWorkbenchConfigurationService, ) { super(WorkspaceTrustEditor.ID, telemetryService, themeService, storageService); } protected createEditor(parent: HTMLElement): void { - this.rootElement = append(parent, $('.workspace-trust-editor', { tabindex: '-1' })); + this.rootElement = append(parent, $('.workspace-trust-editor', { tabindex: '0' })); + this.rootElement.style.visibility = 'hidden'; this.createHeaderElement(this.rootElement); const scrollableContent = $('.workspace-trust-editor-body'); this.bodyScrollBar = this._register(new DomScrollableElement(scrollableContent, { horizontal: ScrollbarVisibility.Hidden, - vertical: ScrollbarVisibility.Visible, + vertical: ScrollbarVisibility.Auto, })); append(this.rootElement, this.bodyScrollBar.getDomNode()); @@ -105,26 +577,24 @@ export class WorkspaceTrustEditor extends EditorPane { this.createAffectedFeaturesElement(scrollableContent); this.createConfigurationElement(scrollableContent); - this._register(attachStylerCallback(this.themeService, { ACTIVITY_BAR_BADGE_BACKGROUND, trustedForegroundColor, untrustedForegroundColor }, colors => { - this.rootElement.style.setProperty('--workspace-trust-trusted-color', colors.trustedForegroundColor?.toString() || ''); - this.rootElement.style.setProperty('--workspace-trust-untrusted-color', colors.untrustedForegroundColor?.toString() || ''); - this.rootElement.style.setProperty('--workspace-trust-selected-state-color', colors.ACTIVITY_BAR_BADGE_BACKGROUND?.toString() || ''); - })); - - this._register(registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { - const foregroundColor = theme.getColor(foreground); - if (foregroundColor) { - const fgWithOpacity = new Color(new RGBA(foregroundColor.rgba.r, foregroundColor.rgba.g, foregroundColor.rgba.b, 0.3)); - collector.addRule(`.workspace-trust-editor .workspace-trust-features .workspace-trust-limitations { border: 1px solid ${fgWithOpacity}; margin: 0px 4px; display: flex; flex-direction: column; padding: 10px 40px;}`); - } + this._register(attachStylerCallback(this.themeService, { debugIconStartForeground, editorErrorForeground, buttonBackground, buttonSecondaryBackground }, colors => { + this.rootElement.style.setProperty('--workspace-trust-selected-color', colors.buttonBackground?.toString() || ''); + this.rootElement.style.setProperty('--workspace-trust-unselected-color', colors.buttonSecondaryBackground?.toString() || ''); + this.rootElement.style.setProperty('--workspace-trust-check-color', colors.debugIconStartForeground?.toString() || ''); + this.rootElement.style.setProperty('--workspace-trust-x-color', colors.editorErrorForeground?.toString() || ''); })); } - override async setInput(input: WorkspaceTrustEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override focus() { + this.rootElement.focus(); + } + + override async setInput(input: WorkspaceTrustEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { await super.setInput(input, options, context, token); if (token.isCancellationRequested) { return; } + await this.workspaceTrustManagementService.workspaceTrustInitialized; this.registerListeners(); this.render(); } @@ -144,17 +614,23 @@ export class WorkspaceTrustEditor extends EditorPane { return 'workspace-trust-header workspace-trust-untrusted'; } - private useWorkspaceLanguage(): boolean { - return !isSingleFolderWorkspaceIdentifier(toWorkspaceIdentifier(this.workspaceService.getWorkspace())); - } - private getHeaderTitleText(trusted: boolean): string { - if (trusted) { - return this.useWorkspaceLanguage() ? localize('trustedHeaderWorkspace', "You trust this workspace") : localize('trustedHeaderFolder', "You trust this folder"); + if (this.workspaceTrustManagementService.isWorkspaceTrustForced()) { + return localize('trustedUnsettableWindow', "This window is trusted"); + } + + switch (this.workspaceService.getWorkbenchState()) { + case WorkbenchState.EMPTY: + return localize('trustedHeaderWindow', "You trust this window"); + case WorkbenchState.FOLDER: + return localize('trustedHeaderFolder', "You trust this folder"); + case WorkbenchState.WORKSPACE: + return localize('trustedHeaderWorkspace', "You trust this workspace"); + } } - return this.useWorkspaceLanguage() ? localize('untrustedHeaderWorkspace', "You are in restricted mode") : localize('untrustedHeaderFolder', "You are in Restricted Mode"); + return localize('untrustedHeader', "You are in Restricted Mode"); } private getHeaderDescriptionText(trusted: boolean): string { @@ -169,6 +645,34 @@ export class WorkspaceTrustEditor extends EditorPane { return shieldIcon.classNamesArray; } + private getFeaturesHeaderText(trusted: boolean): [string, string] { + let title: string = ''; + let subTitle: string = ''; + + switch (this.workspaceService.getWorkbenchState()) { + case WorkbenchState.EMPTY: { + title = trusted ? localize('trustedWindow', "In a trusted window") : localize('untrustedWorkspace', "In Restricted Mode"); + subTitle = trusted ? localize('trustedWindowSubtitle', "You trust the authors of the files in the current window. All features are enabled:") : + localize('untrustedWindowSubtitle', "You do not trust the authors of the files in the current window. The following features are disabled:"); + break; + } + case WorkbenchState.FOLDER: { + title = trusted ? localize('trustedFolder', "In a trusted folder") : localize('untrustedWorkspace', "In Restricted Mode"); + subTitle = trusted ? localize('trustedFolderSubtitle', "You trust the authors of the files in the current folder. All features are enabled:") : + localize('untrustedFolderSubtitle', "You do not trust the authors of the files in the current folder. The following features are disabled:"); + break; + } + case WorkbenchState.WORKSPACE: { + title = trusted ? localize('trustedWorkspace', "In a trusted workspace") : localize('untrustedWorkspace', "In Restricted Mode"); + subTitle = trusted ? localize('trustedWorkspaceSubtitle', "You trust the authors of the files in the current workspace. All features are enabled:") : + localize('untrustedWorkspaceSubtitle', "You do not trust the authors of the files in the current workspace. The following features are disabled:"); + break; + } + } + + return [title, subTitle]; + } + private rendering = false; private rerenderDisposables: DisposableStore = this._register(new DisposableStore()); @debounce(100) @@ -196,17 +700,43 @@ export class WorkspaceTrustEditor extends EditorPane { if (typeof node === 'string') { append(p, document.createTextNode(node)); } else { - const link = this.instantiationService.createInstance(Link, node); + const link = this.instantiationService.createInstance(Link, node, {}); append(p, link.el); this.rerenderDisposables.add(link); - this.rerenderDisposables.add(attachLinkStyler(link, this.themeService)); } } this.headerContainer.className = this.getHeaderContainerClass(isWorkspaceTrusted); + this.rootElement.setAttribute('aria-label', `${localize('root element label', "Manage Workspace Trust")}: ${this.headerContainer.innerText}`); // Settings - const settingsRequiringTrustedWorkspaceCount = filterSettingsRequireWorkspaceTrust(this.configurationService.restrictedSettings.default).length; + const restrictedSettings = this.configurationService.restrictedSettings; + const configurationRegistry = Registry.as(Extensions.Configuration); + const settingsRequiringTrustedWorkspaceCount = restrictedSettings.default.filter(key => { + const property = configurationRegistry.getConfigurationProperties()[key]; + + // cannot be configured in workspace + if (property.scope === ConfigurationScope.APPLICATION || property.scope === ConfigurationScope.MACHINE) { + return false; + } + + // If deprecated include only those configured in the workspace + if (property.deprecationMessage || property.markdownDeprecationMessage) { + if (restrictedSettings.workspace?.includes(key)) { + return true; + } + if (restrictedSettings.workspaceFolder) { + for (const workspaceFolderSettings of restrictedSettings.workspaceFolder.values()) { + if (workspaceFolderSettings.includes(key)) { + return true; + } + } + } + return false; + } + + return true; + }).length; // Features List const installedExtensions = await this.instantiationService.invokeFunction(getInstalledExtensions); @@ -216,19 +746,22 @@ export class WorkspaceTrustEditor extends EditorPane { this.renderAffectedFeatures(settingsRequiringTrustedWorkspaceCount, onDemandExtensionCount + onStartExtensionCount); // Configuration Tree - this.workspaceTrustSettingsTreeModel.update(this.workspaceTrustManagementService.getTrustedFolders()); - this.trustSettingsTree.setChildren(null, Iterable.map(this.workspaceTrustSettingsTreeModel.settings, s => { return { element: s }; })); + this.workpaceTrustedUrisTable.updateTable(); this.bodyScrollBar.getDomNode().style.height = `calc(100% - ${this.headerContainer.clientHeight}px)`; this.bodyScrollBar.scanDomNode(); + this.rootElement.style.visibility = ''; this.rendering = false; } private getExtensionCountByUntrustedWorkspaceSupport(extensions: IExtensionStatus[], trustRequestType: ExtensionUntrustedWorkpaceSupportType): number { const filtered = extensions.filter(ext => this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(ext.local.manifest) === trustRequestType); const set = new Set(); + const inVirtualWorkspace = isVirtualWorkspace(this.workspaceService.getWorkspace()); for (const ext of filtered) { - set.add(ext.identifier.id); + if (!inVirtualWorkspace || this.extensionManifestPropertiesService.getExtensionVirtualWorkspaceSupportType(ext.local.manifest) !== false) { + set.add(ext.identifier.id); + } } return set.size; @@ -243,85 +776,74 @@ export class WorkspaceTrustEditor extends EditorPane { } private createConfigurationElement(parent: HTMLElement): void { - this.configurationContainer = append(parent, $('.workspace-trust-settings.settings-editor')); + this.configurationContainer = append(parent, $('.workspace-trust-settings')); + const configurationTitle = append(this.configurationContainer, $('.workspace-trusted-folders-title')); + configurationTitle.innerText = localize('trustedFoldersAndWorkspaces', "Trusted Folders & Workspaces"); - const settingsBody = append(this.configurationContainer, $('.workspace-trust-settings-body.settings-body')); + const configurationDescription = append(this.configurationContainer, $('.workspace-trusted-folders-description')); + configurationDescription.innerText = localize('trustedFoldersDescription', "You trust the following folders, their children, and workspace files."); - const workspaceTrustTreeContainer = append(settingsBody, $('.workspace-trust-settings-tree-container.settings-tree-container')); - const renderer = this.instantiationService.createInstance(WorkspaceTrustSettingArrayRenderer,); + this.workpaceTrustedUrisTable = this._register(this.instantiationService.createInstance(WorkspaceTrustedUrisTable, this.configurationContainer)); - this.trustSettingsTree = this._register(this.instantiationService.createInstance(WorkspaceTrustTree, - workspaceTrustTreeContainer, - [renderer])); - this.workspaceTrustSettingsTreeModel = this.instantiationService.createInstance(WorkspaceTrustTreeModel); - - this._register(renderer.onDidChangeSetting(e => this.onDidChangeSetting(e))); } private createAffectedFeaturesElement(parent: HTMLElement): void { this.affectedFeaturesContainer = append(parent, $('.workspace-trust-features')); } - private renderAffectedFeatures(numSettings: number, numExtensions: number): void { + private async renderAffectedFeatures(numSettings: number, numExtensions: number): Promise { clearNode(this.affectedFeaturesContainer); + + // Trusted features const trustedContainer = append(this.affectedFeaturesContainer, $('.workspace-trust-limitations.trusted')); - this.renderLimitationsHeaderElement(trustedContainer, - this.useWorkspaceLanguage() ? localize('trustedWorkspace', "In a trusted workspace") : localize('trustedFolder', "In a Trusted Folder"), - this.useWorkspaceLanguage() ? localize('trustedWorkspaceSubtitle', "You trust the authors of the files in the current workspace. All features are enabled:") : localize('trustedFolderSubtitle', "You trust the authors of the files in the current folder. All features are enabled:")); - this.renderLimitationsListElement(trustedContainer, [ - localize('trustedTasks', "Tasks will be allowed to run"), - localize('trustedDebugging', "Debugging will be enabled"), - localize('trustedSettings', "All workspace settings will be applied"), - localize('trustedExtensions', "All extensions will be enabled") - ], checkListIcon.classNamesArray); + const [trustedTitle, trustedSubTitle] = this.getFeaturesHeaderText(true); + this.renderLimitationsHeaderElement(trustedContainer, trustedTitle, trustedSubTitle); + const trustedContainerItems = this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY ? + [ + localize('trustedTasks', "Tasks are allowed to run"), + localize('trustedDebugging', "Debugging is enabled"), + localize('trustedExtensions', "All extensions are enabled") + ] : + [ + localize('trustedTasks', "Tasks are allowed to run"), + localize('trustedDebugging', "Debugging is enabled"), + localize('trustedSettings', "All workspace settings are applied"), + localize('trustedExtensions', "All extensions are enabled") + ]; + this.renderLimitationsListElement(trustedContainer, trustedContainerItems, checkListIcon.classNamesArray); + + // Restricted Mode features const untrustedContainer = append(this.affectedFeaturesContainer, $('.workspace-trust-limitations.untrusted')); - this.renderLimitationsHeaderElement(untrustedContainer, - localize('untrustedWorkspace', "In Restricted Mode"), - this.useWorkspaceLanguage() ? localize('untrustedWorkspaceSubtitle', "You do not trust the authors of the files in the current workspace. The following features are disabled:") : localize('untrustedFolderSubtitle', "You do not trust the authors of the files in the current folder. The following features are disabled:")); + const [untrustedTitle, untrustedSubTitle] = this.getFeaturesHeaderText(false); - this.renderLimitationsListElement(untrustedContainer, [ - localize('untrustedTasks', "Tasks will be disabled"), - localize('untrustedDebugging', "Debugging will be disabled"), - numSettings ? localize('untrustedSettings', "[{0} workspace settings](command:{1}) will not be applied", numSettings, 'settings.filterUntrusted') : localize('no untrustedSettings', "Workspace settings requiring trust will not be applied"), - localize('untrustedExtensions', "[{0} extensions](command:{1}) will be disabled or have limited functionality", numExtensions, 'workbench.extensions.action.listTrustRequiredExtensions') - ], xListIcon.classNamesArray); + this.renderLimitationsHeaderElement(untrustedContainer, untrustedTitle, untrustedSubTitle); + const untrustedContainerItems = this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY ? + [ + localize('untrustedTasks', "Tasks are disabled"), + localize('untrustedDebugging', "Debugging is disabled"), + localize('untrustedExtensions', "[{0} extensions]({1}) are disabled or have limited functionality", numExtensions, `command:${LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID}`) + ] : + [ + localize('untrustedTasks', "Tasks are disabled"), + localize('untrustedDebugging', "Debugging is disabled"), + numSettings ? localize('untrustedSettings', "[{0} workspace settings]({1}) are not applied", numSettings, 'command:settings.filterUntrusted') : localize('no untrustedSettings', "Workspace settings requiring trust are not applied"), + localize('untrustedExtensions', "[{0} extensions]({1}) are disabled or have limited functionality", numExtensions, `command:${LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID}`) + ]; + this.renderLimitationsListElement(untrustedContainer, untrustedContainerItems, xListIcon.classNamesArray); - if (!this.workspaceTrustManagementService.isWorkpaceTrusted()) { - this.addTrustButtonToElement(trustedContainer); - } - - if (this.isTrustedExplicitlyOnly()) { - this.addDontTrustButtonToElement(untrustedContainer); + if (this.workspaceTrustManagementService.isWorkpaceTrusted()) { + if (this.workspaceTrustManagementService.canSetWorkspaceTrust()) { + this.addDontTrustButtonToElement(untrustedContainer); + } else { + this.addTrustedTextToElement(untrustedContainer); + } } else { - this.addTrustedTextToElement(untrustedContainer); - } - } - - private isTrustedExplicitlyOnly(): boolean { - // Can only be trusted explicitly in the single folder scenario - const workspaceIdentifier = toWorkspaceIdentifier(this.workspaceService.getWorkspace()); - if (!(isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && workspaceIdentifier.uri.scheme === Schemas.file)) { - return false; - } - - // If the current folder isn't trusted directly, return false - const trustInfo = this.workspaceTrustManagementService.getFolderTrustInfo(workspaceIdentifier.uri); - if (!trustInfo.trusted || !isEqual(workspaceIdentifier.uri, trustInfo.uri)) { - return false; - } - - // Check if the parent is also trusted - if (this.workspaceTrustManagementService.canSetParentFolderTrust()) { - const { parentPath } = splitName(workspaceIdentifier.uri.fsPath); - const parentIsTrusted = this.workspaceTrustManagementService.getFolderTrustInfo(URI.file(parentPath)).trusted; - if (parentIsTrusted) { - return false; + if (this.workspaceTrustManagementService.canSetWorkspaceTrust()) { + this.addTrustButtonToElement(trustedContainer); } } - - return true; } private createButton(parent: HTMLElement, action: Action, enabled?: boolean): void { @@ -353,58 +875,56 @@ export class WorkspaceTrustEditor extends EditorPane { } private addTrustButtonToElement(parent: HTMLElement): void { - if (this.workspaceTrustManagementService.canSetWorkspaceTrust()) { - - const trustUris = async (uris?: URI[]) => { - if (!uris) { - this.workspaceTrustManagementService.setWorkspaceTrust(true); - } else { - this.workspaceTrustManagementService.setFoldersTrust(uris, true); - } - }; - - const trustChoiceWithMenu: IPromptChoiceWithMenu = { - isSecondary: false, - label: localize('trustButton', "Trust"), - menu: [], - run: () => { - trustUris(); - } - }; - - const workspaceIdentifier = toWorkspaceIdentifier(this.workspaceService.getWorkspace()); - if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && workspaceIdentifier.uri.scheme === Schemas.file) { - const { parentPath } = splitName(workspaceIdentifier.uri.fsPath); - if (parentPath) { - trustChoiceWithMenu.menu.push({ - label: localize('trustParentButton', "Trust All in Parent Folder"), - run: () => { - trustUris([URI.file(parentPath)]); - } - }); - } + const trustUris = async (uris?: URI[]) => { + if (!uris) { + await this.workspaceTrustManagementService.setWorkspaceTrust(true); + } else { + await this.workspaceTrustManagementService.setUrisTrust(uris, true); } + }; - const isWorkspaceTrusted = this.workspaceTrustManagementService.isWorkpaceTrusted(); - this.createButton(parent, new ChoiceAction('workspace.trust.button.action', trustChoiceWithMenu), !isWorkspaceTrusted); + const trustChoiceWithMenu: IPromptChoiceWithMenu = { + isSecondary: false, + label: localize('trustButton', "Trust"), + menu: [], + run: () => { + trustUris(); + } + }; + + const workspaceIdentifier = toWorkspaceIdentifier(this.workspaceService.getWorkspace()); + if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && workspaceIdentifier.uri.scheme === Schemas.file) { + const { parentPath } = splitName(workspaceIdentifier.uri.fsPath); + if (parentPath) { + trustChoiceWithMenu.menu.push({ + label: localize('trustParentButton', "Trust All in Parent Folder"), + run: () => { + trustUris([URI.file(parentPath)]); + } + }); + } } + + const isWorkspaceTrusted = this.workspaceTrustManagementService.isWorkpaceTrusted(); + this.createButton(parent, new ChoiceAction('workspace.trust.button.action', trustChoiceWithMenu), !isWorkspaceTrusted); } private addDontTrustButtonToElement(parent: HTMLElement): void { - if (this.workspaceTrustManagementService.canSetWorkspaceTrust() && this.isTrustedExplicitlyOnly()) { - this.createButton(parent, new Action('workspace.trust.button.action.deny', localize('dontTrustButton', "Don't Trust"), undefined, true, async () => { - await this.workspaceTrustManagementService.setWorkspaceTrust(false); - })); - } + this.createButton(parent, new Action('workspace.trust.button.action.deny', localize('dontTrustButton', "Don't Trust"), undefined, true, async () => { + await this.workspaceTrustManagementService.setWorkspaceTrust(false); + })); } private addTrustedTextToElement(parent: HTMLElement): void { - const isWorkspaceTrusted = this.workspaceTrustManagementService.isWorkpaceTrusted(); - const canSetWorkspaceTrust = this.workspaceTrustManagementService.canSetWorkspaceTrust(); + if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) { + return; + } - if (canSetWorkspaceTrust && isWorkspaceTrusted) { - const textElement = append(parent, $('.workspace-trust-untrusted-description')); - textElement.innerText = this.useWorkspaceLanguage() ? localize('untrustedWorkspaceReason', "This workspace is trusted via one or more of the trusted folders below.") : localize('untrustedFolderReason', "This folder is trusted via one or more of the trusted folders below."); + const textElement = append(parent, $('.workspace-trust-untrusted-description')); + if (!this.workspaceTrustManagementService.isWorkspaceTrustForced()) { + textElement.innerText = this.workspaceService.getWorkbenchState() === WorkbenchState.WORKSPACE ? localize('untrustedWorkspaceReason', "This workspace is trusted via the bolded entries in the trusted folders below.") : localize('untrustedFolderReason', "This folder is trusted via the bolded entries in the the trusted folders below."); + } else { + textElement.innerText = localize('trustedForcedReason', "This window is trusted by nature of the workspace that is opened."); } } @@ -433,46 +953,21 @@ export class WorkspaceTrustEditor extends EditorPane { if (typeof node === 'string') { append(text, document.createTextNode(node)); } else { - const link = this.instantiationService.createInstance(Link, node); + const link = this.instantiationService.createInstance(Link, node, {}); append(text, link.el); this.rerenderDisposables.add(link); - this.rerenderDisposables.add(attachLinkStyler(link, this.themeService)); } } } } - private onDidChangeSetting(change: IWorkspaceTrustSettingChangeEvent) { - const applyChangesWithPrompt = async (showPrompt: boolean, applyChanges: () => void) => { - if (showPrompt) { - const message = localize('workspaceTrustSettingModificationMessage', "Update Workspace Trust Settings"); - const detail = localize('workspaceTrustTransitionDetail', "In order to safely complete this action, all affected windows will have to be reloaded. Are you sure you want to proceed with this action?"); - const primaryButton = localize('workspaceTrustTransitionPrimaryButton', "Yes"); - const secondaryButton = localize('workspaceTrustTransitionSecondaryButton', "No"); - - const result = await this.dialogService.show(Severity.Info, message, [primaryButton, secondaryButton], { cancelId: 1, detail, custom: { icon: Codicon.shield } }); - if (result.choice !== 0) { - return; - } - } - - applyChanges(); - }; - - if (isArray(change.value)) { - if (change.key === 'trustedFolders') { - applyChangesWithPrompt(false, () => this.workspaceTrustManagementService.setTrustedFolders(change.value!)); - } - } - } - private layoutParticipants: { layout: () => void; }[] = []; layout(dimension: Dimension): void { if (!this.isVisible()) { return; } - this.trustSettingsTree.layout(dimension.height, dimension.width); + this.workpaceTrustedUrisTable.layout(); this.layoutParticipants.forEach(participant => { participant.layout(); diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustTree.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustTree.ts deleted file mode 100644 index 94f24228f0..0000000000 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustTree.ts +++ /dev/null @@ -1,624 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { addDisposableListener, append, EventType, $, createStyleSheet, trackFocus, addStandardDisposableListener } from 'vs/base/browser/dom'; -import { DefaultStyleController, IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { IList } from 'vs/base/browser/ui/tree/indexTreeModel'; -import { IObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree'; -import { ITreeModel, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; -import { Color, RGBA } from 'vs/base/common/color'; -import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; -import { isArray } from 'vs/base/common/types'; -import { localize } from 'vs/nls'; -import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IListService, WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { editorBackground, errorForeground, focusBorder, foreground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry'; -import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { NonCollapsibleObjectTreeModel } from 'vs/workbench/contrib/preferences/browser/settingsTree'; -import { AbstractListSettingWidget, focusedRowBackground, focusedRowBorder, ISettingListChangeEvent, rowHoverBackground, settingsHeaderForeground, settingsSelectBackground, settingsTextInputBorder, settingsTextInputForeground } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; -import { attachButtonStyler, attachInputBoxStyler, attachStyler } from 'vs/platform/theme/common/styler'; -import { CachedListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { IAction } from 'vs/base/common/actions'; -import { settingsEditIcon, settingsRemoveIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; -import { Button } from 'vs/base/browser/ui/button/button'; -import { disposableTimeout } from 'vs/base/common/async'; -import { URI, UriComponents } from 'vs/base/common/uri'; -import { Schemas } from 'vs/base/common/network'; -import { ILabelService } from 'vs/platform/label/common/label'; - - -export class WorkspaceTrustSettingsTreeEntry { - id: string; - displayLabel: string; - setting: { - key: string; - description: string; - }; - value: URI[]; - - constructor(key: string, displayLabel: string, description: string, value: URI[]) { - this.setting = { key, description }; - this.displayLabel = displayLabel; - this.value = value; - this.id = key; - } -} - -export interface IWorkspaceTrustSettingItemTemplate { - onChange?: (value: T, type: WorkspaceTrustSettingListItemChangeType) => void; - - toDispose: DisposableStore; - context?: WorkspaceTrustSettingsTreeEntry; - containerElement: HTMLElement; - labelElement: HTMLElement; - descriptionElement: HTMLElement; - controlElement: HTMLElement; - elementDisposables: DisposableStore; -} - -export interface IWorkspaceTrustUriDataItem extends UriComponents { } - -class WorkspaceTrustFolderSettingWidget extends AbstractListSettingWidget { - constructor( - container: HTMLElement, - @ILabelService protected readonly labelService: ILabelService, - @IThemeService themeService: IThemeService, - @IContextViewService contextViewService: IContextViewService - ) { - super(container, themeService, contextViewService); - } - - protected getEmptyItem(): IWorkspaceTrustUriDataItem { - return URI.file(''); - } - - protected getContainerClasses() { - return ['workspace-trust-uri-setting-widget', 'setting-list-object-widget']; - } - - protected getActionsForItem(item: IWorkspaceTrustUriDataItem, idx: number): IAction[] { - return [ - { - class: ThemeIcon.asClassName(settingsEditIcon), - enabled: true, - id: 'workbench.action.editListItem', - tooltip: this.getLocalizedStrings().editActionTooltip, - run: () => this.editSetting(idx) - }, - { - class: ThemeIcon.asClassName(settingsRemoveIcon), - enabled: true, - id: 'workbench.action.removeListItem', - tooltip: this.getLocalizedStrings().deleteActionTooltip, - run: () => this._onDidChangeList.fire({ originalItem: item, item: undefined, targetIndex: idx }) - } - ] as IAction[]; - } - - protected override renderHeader() { - const header = $('.setting-list-row-header'); - const hostHeader = append(header, $('.setting-list-object-key')); - const pathHeader = append(header, $('.setting-list-object-value')); - const { hostHeaderText, pathHeaderText } = this.getLocalizedStrings(); - - hostHeader.textContent = hostHeaderText; - pathHeader.textContent = pathHeaderText; - - return header; - } - - protected renderItem(item: IWorkspaceTrustUriDataItem): HTMLElement { - const rowElement = $('.setting-list-row'); - rowElement.classList.add('setting-list-object-row'); - - const hostElement = append(rowElement, $('.setting-list-object-key')); - const pathElement = append(rowElement, $('.setting-list-object-value')); - - hostElement.textContent = item.authority ? this.labelService.getHostLabel(item.scheme, item.authority) : localize('localAuthority', "Local"); - pathElement.textContent = item.scheme === Schemas.file ? URI.revive(item).fsPath : item.path; - - return rowElement; - } - - - protected renderEdit(item: IWorkspaceTrustUriDataItem, idx: number): HTMLElement { - const rowElement = $('.setting-list-edit-row'); - - const hostElement = append(rowElement, $('.setting-list-object-key')); - hostElement.textContent = item.authority ? this.labelService.getHostLabel(item.scheme, item.authority) : localize('localAuthority', "Local"); - - const updatedItem = () => { - if (item.scheme === Schemas.file) { - return URI.file(pathInput.value); - } else { - return URI.revive(item).with({ path: pathInput.value }); - } - }; - - const onKeyDown = (e: StandardKeyboardEvent) => { - if (e.equals(KeyCode.Enter)) { - this.handleItemChange(item, updatedItem(), idx); - } else if (e.equals(KeyCode.Escape)) { - this.cancelEdit(); - e.preventDefault(); - } - rowElement?.focus(); - }; - - const pathInput = new InputBox(rowElement, this.contextViewService, { - placeholder: this.getLocalizedStrings().inputPlaceholder - }); - - pathInput.element.classList.add('setting-list-valueInput'); - this.listDisposables.add(attachInputBoxStyler(pathInput, this.themeService, { - inputBackground: settingsSelectBackground, - inputForeground: settingsTextInputForeground, - inputBorder: settingsTextInputBorder - })); - this.listDisposables.add(pathInput); - pathInput.value = item.scheme === Schemas.file ? URI.revive(item).fsPath : item.path; - - this.listDisposables.add( - addStandardDisposableListener(pathInput.inputElement, EventType.KEY_DOWN, onKeyDown) - ); - - const okButton = this._register(new Button(rowElement)); - okButton.label = localize('okButton', "OK"); - okButton.element.classList.add('setting-list-ok-button'); - - this.listDisposables.add(attachButtonStyler(okButton, this.themeService)); - this.listDisposables.add(okButton.onDidClick(() => this.handleItemChange(item, updatedItem(), idx))); - - const cancelButton = this._register(new Button(rowElement)); - cancelButton.label = localize('cancelButton', "Cancel"); - cancelButton.element.classList.add('setting-list-cancel-button'); - - this.listDisposables.add(attachButtonStyler(cancelButton, this.themeService)); - this.listDisposables.add(cancelButton.onDidClick(() => this.cancelEdit())); - - this.listDisposables.add( - disposableTimeout(() => { - pathInput.focus(); - pathInput.select(); - }) - ); - - return rowElement; - } - - protected isItemNew(item: IWorkspaceTrustUriDataItem): boolean { - return item.path === ''; - } - - protected getLocalizedRowTitle(item: IWorkspaceTrustUriDataItem): string { - return localize('trustedRow', "Trusted Path: {0}", this.labelService.getUriLabel(URI.from(item))); - } - - protected getLocalizedStrings() { - return { - deleteActionTooltip: localize('removePath', "Remove Path"), - editActionTooltip: localize('editPath', "Edit Path"), - addButtonLabel: localize('addPath', "Add Path"), - hostHeaderText: localize('hostHeaderText', "Host"), - pathHeaderText: localize('pathHeaderText', "Path"), - inputPlaceholder: localize('pathInputPlaceholder', "Path Item..."), - }; - } -} - -interface IWorkspaceTrustSettingListItemTemplate extends IWorkspaceTrustSettingItemTemplate { - listWidget: WorkspaceTrustFolderSettingWidget; - validationErrorMessageElement: HTMLElement; -} - -export type WorkspaceTrustSettingListItemChangeType = 'added' | 'removed' | 'changed'; -export interface IWorkspaceTrustSettingChangeEvent { - key: string; - value: URI[] | undefined; // undefined => reset/unconfigure - type: WorkspaceTrustSettingListItemChangeType; -} - - -export class WorkspaceTrustSettingArrayRenderer extends Disposable implements ITreeRenderer { - templateId = 'template.setting.array'; - - static readonly CONTROL_CLASS = 'setting-control-focus-target'; - static readonly CONTROL_SELECTOR = '.' + WorkspaceTrustSettingArrayRenderer.CONTROL_CLASS; - static readonly CONTENTS_CLASS = 'setting-item-contents'; - static readonly CONTENTS_SELECTOR = '.' + WorkspaceTrustSettingArrayRenderer.CONTENTS_CLASS; - static readonly ALL_ROWS_SELECTOR = '.monaco-list-row'; - - static readonly SETTING_KEY_ATTR = 'data-key'; - static readonly SETTING_ID_ATTR = 'data-id'; - static readonly ELEMENT_FOCUSABLE_ATTR = 'data-focusable'; - - protected readonly _onDidChangeSetting = this._register(new Emitter()); - readonly onDidChangeSetting: Event = this._onDidChangeSetting.event; - - private readonly _onDidFocusSetting = this._register(new Emitter()); - readonly onDidFocusSetting: Event = this._onDidFocusSetting.event; - - private readonly _onDidChangeIgnoredSettings = this._register(new Emitter()); - readonly onDidChangeIgnoredSettings: Event = this._onDidChangeIgnoredSettings.event; - - constructor( - @IThemeService protected readonly _themeService: IThemeService, - @IContextViewService protected readonly _contextViewService: IContextViewService, - @IOpenerService protected readonly _openerService: IOpenerService, - @IInstantiationService protected readonly _instantiationService: IInstantiationService, - @ICommandService protected readonly _commandService: ICommandService, - @IContextMenuService protected readonly _contextMenuService: IContextMenuService, - @IKeybindingService protected readonly _keybindingService: IKeybindingService, - @IConfigurationService protected readonly _configService: IConfigurationService, - ) { - super(); - } - - renderCommonTemplate(tree: any, _container: HTMLElement, typeClass: string): IWorkspaceTrustSettingItemTemplate { - _container.classList.add('setting-item'); - _container.classList.add('setting-item-' + typeClass); - - const container = append(_container, $(WorkspaceTrustSettingArrayRenderer.CONTENTS_SELECTOR)); - container.classList.add('settings-row-inner-container'); - const titleElement = append(container, $('.setting-item-title')); - const labelCategoryContainer = append(titleElement, $('.setting-item-cat-label-container')); - const labelElement = append(labelCategoryContainer, $('span.setting-item-label')); - const descriptionElement = append(container, $('.setting-item-description')); - const modifiedIndicatorElement = append(container, $('.setting-item-modified-indicator')); - modifiedIndicatorElement.title = localize('modified', "Modified"); - - const valueElement = append(container, $('.setting-item-value')); - const controlElement = append(valueElement, $('div.setting-item-control')); - const toDispose = new DisposableStore(); - - const template: IWorkspaceTrustSettingItemTemplate = { - toDispose, - elementDisposables: new DisposableStore(), - containerElement: container, - labelElement, - descriptionElement, - controlElement - }; - - // Prevent clicks from being handled by list - toDispose.add(addDisposableListener(controlElement, EventType.MOUSE_DOWN, e => e.stopPropagation())); - - toDispose.add(addDisposableListener(titleElement, EventType.MOUSE_ENTER, e => container.classList.add('mouseover'))); - toDispose.add(addDisposableListener(titleElement, EventType.MOUSE_LEAVE, e => container.classList.remove('mouseover'))); - - return template; - } - - addSettingElementFocusHandler(template: IWorkspaceTrustSettingItemTemplate): void { - const focusTracker = trackFocus(template.containerElement); - template.toDispose.add(focusTracker); - focusTracker.onDidBlur(() => { - if (template.containerElement.classList.contains('focused')) { - template.containerElement.classList.remove('focused'); - } - }); - - focusTracker.onDidFocus(() => { - template.containerElement.classList.add('focused'); - - if (template.context) { - this._onDidFocusSetting.fire(template.context); - } - }); - } - - renderTemplate(container: HTMLElement): IWorkspaceTrustSettingListItemTemplate { - const common = this.renderCommonTemplate(null, container, 'list'); - const descriptionElement = common.containerElement.querySelector('.setting-item-description')!; - const validationErrorMessageElement = $('.setting-item-validation-message'); - descriptionElement.after(validationErrorMessageElement); - - const listWidget = this._instantiationService.createInstance(WorkspaceTrustFolderSettingWidget, common.controlElement); - listWidget.domNode.classList.add(WorkspaceTrustSettingArrayRenderer.CONTROL_CLASS); - common.toDispose.add(listWidget); - - const template: IWorkspaceTrustSettingListItemTemplate = { - ...common, - listWidget, - validationErrorMessageElement - }; - - this.addSettingElementFocusHandler(template); - - common.toDispose.add( - listWidget.onDidChangeList(e => { - const { list: newList, changeType } = this.computeNewList(template, e); - if (newList !== null && template.onChange) { - template.onChange(newList, changeType); - } - }) - ); - - return template; - } - - private computeNewList(template: IWorkspaceTrustSettingListItemTemplate, e: ISettingListChangeEvent): { list: URI[] | null, changeType: WorkspaceTrustSettingListItemChangeType } { - if (template.context) { - let newValue: URI[] = []; - - let changeType: WorkspaceTrustSettingListItemChangeType = 'changed'; - if (isArray(template.context.value)) { - newValue = [...template.context.value]; - } - - if (e.targetIndex !== undefined) { - // Delete value - if (!e.item?.path && e.originalItem.path && e.targetIndex > -1) { - newValue.splice(e.targetIndex, 1); - changeType = 'removed'; - } - // Update value - else if (e.item?.path && e.originalItem.path) { - if (e.targetIndex > -1) { - newValue[e.targetIndex] = URI.revive(e.item); - changeType = e.targetIndex < template.context.value.length ? 'changed' : 'added'; - } - // For some reason, we are updating and cannot find original value - // Just append the value in this case - else { - newValue.push(URI.revive(e.item)); - changeType = 'added'; - } - } - // Add value - else if (e.item?.path && !e.originalItem.path && e.targetIndex >= newValue.length) { - newValue.push(URI.revive(e.item)); - changeType = 'added'; - } - } - - return { list: newValue, changeType }; - } - - return { list: null, changeType: 'changed' }; - } - - renderElement(node: ITreeNode, index: number, template: IWorkspaceTrustSettingListItemTemplate): void { - const element = node.element; - template.context = element; - - template.containerElement.setAttribute(WorkspaceTrustSettingArrayRenderer.SETTING_KEY_ATTR, element.setting.key); - template.containerElement.setAttribute(WorkspaceTrustSettingArrayRenderer.SETTING_ID_ATTR, element.id); - - template.labelElement.textContent = element.displayLabel; - - template.descriptionElement.innerText = element.setting.description; - - const onChange = (value: any, type: WorkspaceTrustSettingListItemChangeType) => this._onDidChangeSetting.fire({ key: element.setting.key, value, type }); - this.renderValue(element, template, onChange); - } - - protected renderValue(dataElement: WorkspaceTrustSettingsTreeEntry, template: IWorkspaceTrustSettingListItemTemplate, onChange: (value: URI[] | undefined, type: WorkspaceTrustSettingListItemChangeType) => void): void { - const value = getListDisplayValue(dataElement); - template.listWidget.setValue(value); - template.context = dataElement; - - template.onChange = (v, t) => { - onChange(v, t); - renderArrayValidations(dataElement, template, v, false); - }; - - renderArrayValidations(dataElement, template, value.map(v => URI.revive(v)), true); - } - - disposeTemplate(template: IWorkspaceTrustSettingItemTemplate): void { - dispose(template.toDispose); - } - - disposeElement(_element: ITreeNode, _index: number, template: IWorkspaceTrustSettingItemTemplate, _height: number | undefined): void { - if (template.elementDisposables) { - template.elementDisposables.clear(); - } - } -} - -export class WorkspaceTrustTree extends WorkbenchObjectTree { - constructor( - container: HTMLElement, - renderers: ITreeRenderer[], - @IContextKeyService contextKeyService: IContextKeyService, - @IListService listService: IListService, - @IThemeService themeService: IThemeService, - @IConfigurationService configurationService: IConfigurationService, - @IKeybindingService keybindingService: IKeybindingService, - @IAccessibilityService accessibilityService: IAccessibilityService, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super('WorkspaceTrustTree', container, - new WorkspaceTrustTreeDelegate(), - renderers, - { - horizontalScrolling: false, - alwaysConsumeMouseWheel: false, - supportDynamicHeights: true, - identityProvider: { - getId(e) { - return e.id; - } - }, - accessibilityProvider: new WorkspaceTrustTreeAccessibilityProvider(), - styleController: id => new DefaultStyleController(createStyleSheet(container), id), - smoothScrolling: configurationService.getValue('workbench.list.smoothScrolling'), - multipleSelectionSupport: false, - }, - contextKeyService, - listService, - themeService, - configurationService, - keybindingService, - accessibilityService, - ); - - this.disposables.add(registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { - const foregroundColor = theme.getColor(foreground); - if (foregroundColor) { - // Links appear inside other elements in markdown. CSS opacity acts like a mask. So we have to dynamically compute the description color to avoid - // applying an opacity to the link color. - const fgWithOpacity = new Color(new RGBA(foregroundColor.rgba.r, foregroundColor.rgba.g, foregroundColor.rgba.b, 0.9)); - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-description { color: ${fgWithOpacity}; }`); - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .settings-toc-container .monaco-list-row:not(.selected) { color: ${fgWithOpacity}; }`); - - // Hack for subpixel antialiasing - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-title .setting-item-overrides, - .workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-title .setting-item-ignored { color: ${fgWithOpacity}; }`); - } - - const errorColor = theme.getColor(errorForeground); - if (errorColor) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-deprecation-message { color: ${errorColor}; }`); - } - - const invalidInputBackground = theme.getColor(inputValidationErrorBackground); - if (invalidInputBackground) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-validation-message { background-color: ${invalidInputBackground}; }`); - } - - const invalidInputForeground = theme.getColor(inputValidationErrorForeground); - if (invalidInputForeground) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-validation-message { color: ${invalidInputForeground}; }`); - } - - const invalidInputBorder = theme.getColor(inputValidationErrorBorder); - if (invalidInputBorder) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-validation-message { border-style:solid; border-width: 1px; border-color: ${invalidInputBorder}; }`); - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item.invalid-input .setting-item-control .monaco-inputbox.idle { outline-width: 0; border-style:solid; border-width: 1px; border-color: ${invalidInputBorder}; }`); - } - - const focusedRowBackgroundColor = theme.getColor(focusedRowBackground); - if (focusedRowBackgroundColor) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .monaco-list-row.focused .settings-row-inner-container { background-color: ${focusedRowBackgroundColor}; }`); - } - - const rowHoverBackgroundColor = theme.getColor(rowHoverBackground); - if (rowHoverBackgroundColor) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .monaco-list-row:not(.focused) .settings-row-inner-container:hover { background-color: ${rowHoverBackgroundColor}; }`); - } - - const focusedRowBorderColor = theme.getColor(focusedRowBorder); - if (focusedRowBorderColor) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .setting-item-contents::before, - .workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .setting-item-contents::after { border-top: 1px solid ${focusedRowBorderColor} }`); - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .settings-group-title-label::before, - .workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .settings-group-title-label::after { border-top: 1px solid ${focusedRowBorderColor} }`); - } - - const headerForegroundColor = theme.getColor(settingsHeaderForeground); - if (headerForegroundColor) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .settings-group-title-label { color: ${headerForegroundColor}; }`); - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-label { color: ${headerForegroundColor}; }`); - } - - const focusBorderColor = theme.getColor(focusBorder); - if (focusBorderColor) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-markdown a:focus { outline-color: ${focusBorderColor} }`); - } - })); - - this.getHTMLElement().classList.add('settings-editor-tree'); - - this.disposables.add(attachStyler(themeService, { - listBackground: editorBackground, - listActiveSelectionBackground: editorBackground, - listActiveSelectionForeground: foreground, - listFocusAndSelectionBackground: editorBackground, - listFocusAndSelectionForeground: foreground, - listFocusBackground: editorBackground, - listFocusForeground: foreground, - listHoverForeground: foreground, - listHoverBackground: editorBackground, - listHoverOutline: editorBackground, - listFocusOutline: editorBackground, - listInactiveSelectionBackground: editorBackground, - listInactiveSelectionForeground: foreground, - listInactiveFocusBackground: editorBackground, - listInactiveFocusOutline: editorBackground - }, colors => { - this.style(colors); - })); - - this.disposables.add(configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('workbench.list.smoothScrolling')) { - this.updateOptions({ - smoothScrolling: configurationService.getValue('workbench.list.smoothScrolling') - }); - } - })); - } - - protected override createModel(user: string, view: IList>, options: IObjectTreeOptions): ITreeModel { - return new NonCollapsibleObjectTreeModel(user, view, options); - } -} - -export class WorkspaceTrustTreeModel { - - settings: WorkspaceTrustSettingsTreeEntry[] = []; - - update(trustedFolders: URI[]): void { - this.settings = []; - this.settings.push(new WorkspaceTrustSettingsTreeEntry( - 'trustedFolders', - localize('trustedFolders', "Trusted Folders"), - localize('trustedFoldersDescription', "You trust the following folders and their children: "), - trustedFolders)); - } -} - -class WorkspaceTrustTreeAccessibilityProvider implements IListAccessibilityProvider { - getAriaLabel(element: WorkspaceTrustSettingsTreeEntry) { - if (element instanceof WorkspaceTrustSettingsTreeEntry) { - return `element.displayLabel`; - } - - return null; - } - - getWidgetAriaLabel() { - return localize('settings', "Workspace Trust Setting"); - } -} - -class WorkspaceTrustTreeDelegate extends CachedListVirtualDelegate { - - getTemplateId(element: WorkspaceTrustSettingsTreeEntry): string { - return 'template.setting.array'; - } - - hasDynamicHeight(element: WorkspaceTrustSettingsTreeEntry): boolean { - return true; - } - - protected estimateHeight(element: WorkspaceTrustSettingsTreeEntry): number { - return 104; - } -} - -function getListDisplayValue(element: WorkspaceTrustSettingsTreeEntry): IWorkspaceTrustUriDataItem[] { - if (!element.value || !isArray(element.value)) { - return []; - } - - return element.value; -} - -function renderArrayValidations(dataElement: WorkspaceTrustSettingsTreeEntry, template: IWorkspaceTrustSettingListItemTemplate, v: URI[] | undefined, arg3: boolean) { -} - diff --git a/src/vs/workbench/contrib/workspaces/browser/workspaces.contribution.ts b/src/vs/workbench/contrib/workspaces/browser/workspaces.contribution.ts index 3fba6f06a2..6181db9252 100644 --- a/src/vs/workbench/contrib/workspaces/browser/workspaces.contribution.ts +++ b/src/vs/workbench/contrib/workspaces/browser/workspaces.contribution.ts @@ -16,6 +16,7 @@ import { joinPath } from 'vs/base/common/resources'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; /** * A workbench contribution that will look for `.code-workspace` files in the root of the @@ -28,7 +29,8 @@ export class WorkspacesFinderContribution extends Disposable implements IWorkben @INotificationService private readonly notificationService: INotificationService, @IFileService private readonly fileService: IFileService, @IQuickInputService private readonly quickInputService: IQuickInputService, - @IHostService private readonly hostService: IHostService + @IHostService private readonly hostService: IHostService, + @IStorageService private readonly storageService: IStorageService ) { super(); @@ -60,7 +62,10 @@ export class WorkspacesFinderContribution extends Disposable implements IWorkben this.notificationService.prompt(Severity.Info, localize('workspaceFound', "This folder contains a workspace file '{0}'. Do you want to open it? [Learn more]({1}) about workspace files.", workspaceFile, 'https://go.microsoft.com/fwlink/?linkid=2025315'), [{ label: localize('openWorkspace', "Open Workspace"), run: () => this.hostService.openWindow([{ workspaceUri: joinPath(folder, workspaceFile) }]) - }], { neverShowAgain }); + }], { + neverShowAgain, + silent: !this.storageService.isNew(StorageScope.WORKSPACE) // https://github.com/microsoft/vscode/issues/125315 + }); } // Prompt to select a workspace from many @@ -76,7 +81,10 @@ export class WorkspacesFinderContribution extends Disposable implements IWorkben } }); } - }], { neverShowAgain }); + }], { + neverShowAgain, + silent: !this.storageService.isNew(StorageScope.WORKSPACE) // https://github.com/microsoft/vscode/issues/125315 + }); } } } diff --git a/src/vs/workbench/electron-sandbox/actions/installActions.ts b/src/vs/workbench/electron-sandbox/actions/installActions.ts new file mode 100644 index 0000000000..427ae4be3b --- /dev/null +++ b/src/vs/workbench/electron-sandbox/actions/installActions.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 { localize } from 'vs/nls'; +import Severity from 'vs/base/common/severity'; +import { Action2, ILocalizedString } from 'vs/platform/actions/common/actions'; +import product from 'vs/platform/product/common/product'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { IProductService } from 'vs/platform/product/common/productService'; + +const shellCommandCategory: ILocalizedString = { value: localize('shellCommand', "Shell Command"), original: 'Shell Command' }; + +export class InstallShellScriptAction extends Action2 { + + constructor() { + super({ + id: 'workbench.action.installCommandLine', + title: { + value: localize('install', "Install '{0}' command in PATH", product.applicationName), + original: `Install \'${product.applicationName}\' command in PATH` + }, + category: shellCommandCategory, + f1: true + }); + } + + async run(accessor: ServicesAccessor): Promise { + const nativeHostService = accessor.get(INativeHostService); + const dialogService = accessor.get(IDialogService); + const productService = accessor.get(IProductService); + + try { + await nativeHostService.installShellCommand(); + + dialogService.show(Severity.Info, localize('successIn', "Shell command '{0}' successfully installed in PATH.", productService.applicationName)); + } catch (error) { + dialogService.show(Severity.Error, toErrorMessage(error)); + } + } +} + +export class UninstallShellScriptAction extends Action2 { + + constructor() { + super({ + id: 'workbench.action.uninstallCommandLine', + title: { + value: localize('uninstall', "Uninstall '{0}' command from PATH", product.applicationName), + original: `Uninstall \'${product.applicationName}\' command from PATH` + }, + category: shellCommandCategory, + f1: true + }); + } + + async run(accessor: ServicesAccessor): Promise { + const nativeHostService = accessor.get(INativeHostService); + const dialogService = accessor.get(IDialogService); + const productService = accessor.get(IProductService); + + try { + await nativeHostService.uninstallShellCommand(); + + dialogService.show(Severity.Info, localize('successFrom', "Shell command '{0}' successfully uninstalled from PATH.", productService.applicationName)); + } catch (error) { + dialogService.show(Severity.Error, toErrorMessage(error)); + } + } +} diff --git a/src/vs/workbench/electron-sandbox/actions/windowActions.ts b/src/vs/workbench/electron-sandbox/actions/windowActions.ts index 8cbc55a44a..a3ed1837f7 100644 --- a/src/vs/workbench/electron-sandbox/actions/windowActions.ts +++ b/src/vs/workbench/electron-sandbox/actions/windowActions.ts @@ -5,7 +5,6 @@ import 'vs/css!./media/actions'; import { URI } from 'vs/base/common/uri'; -import { Action } from 'vs/base/common/actions'; import { localize } from 'vs/nls'; import { applyZoom } from 'vs/platform/windows/electron-sandbox/window'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -21,48 +20,64 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { Codicon } from 'vs/base/common/codicons'; import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { Action2, IAction2Options, MenuId } from 'vs/platform/actions/common/actions'; +import { CATEGORIES } from 'vs/workbench/common/actions'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -export class CloseCurrentWindowAction extends Action { +export class CloseWindowAction extends Action2 { - static readonly ID = 'workbench.action.closeWindow'; - static readonly LABEL = localize('closeWindow', "Close Window"); - - constructor( - id: string, - label: string, - @INativeHostService private readonly nativeHostService: INativeHostService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.closeWindow', + title: { + value: localize('closeWindow', "Close Window"), + mnemonicTitle: localize({ key: 'miCloseWindow', comment: ['&& denotes a mnemonic'] }, "Clos&&e Window"), + original: 'Close Window' + }, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_W }, + linux: { primary: KeyMod.Alt | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_W] }, + win: { primary: KeyMod.Alt | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_W] } + }, + menu: { + id: MenuId.MenubarFileMenu, + group: '6_close', + order: 4 + } + }); } - override async run(): Promise { - this.nativeHostService.closeWindow(); + override async run(accessor: ServicesAccessor): Promise { + const nativeHostService = accessor.get(INativeHostService); + + return nativeHostService.closeWindow(); } } -export abstract class BaseZoomAction extends Action { +abstract class BaseZoomAction extends Action2 { private static readonly SETTING_KEY = 'window.zoomLevel'; - private static readonly MAX_ZOOM_LEVEL = 9; + private static readonly MAX_ZOOM_LEVEL = 8; private static readonly MIN_ZOOM_LEVEL = -8; - constructor( - id: string, - label: string, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super(id, label); + constructor(desc: Readonly) { + super(desc); } - protected async setConfiguredZoomLevel(level: number): Promise { + protected async setConfiguredZoomLevel(accessor: ServicesAccessor, level: number): Promise { + const configurationService = accessor.get(IConfigurationService); + level = Math.round(level); // when reaching smallest zoom, prevent fractional zoom levels if (level > BaseZoomAction.MAX_ZOOM_LEVEL || level < BaseZoomAction.MIN_ZOOM_LEVEL) { return; // https://github.com/microsoft/vscode/issues/48357 } - await this.configurationService.updateValue(BaseZoomAction.SETTING_KEY, level); + await configurationService.updateValue(BaseZoomAction.SETTING_KEY, level); applyZoom(level); } @@ -70,59 +85,98 @@ export abstract class BaseZoomAction extends Action { export class ZoomInAction extends BaseZoomAction { - static readonly ID = 'workbench.action.zoomIn'; - static readonly LABEL = localize('zoomIn', "Zoom In"); - - constructor( - id: string, - label: string, - @IConfigurationService configurationService: IConfigurationService - ) { - super(id, label, configurationService); + constructor() { + super({ + id: 'workbench.action.zoomIn', + title: { + value: localize('zoomIn', "Zoom In"), + mnemonicTitle: localize({ key: 'miZoomIn', comment: ['&& denotes a mnemonic'] }, "&&Zoom In"), + original: 'Zoom In' + }, + category: CATEGORIES.View.value, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.US_EQUAL, + secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_EQUAL, KeyMod.CtrlCmd | KeyCode.NUMPAD_ADD] + }, + menu: { + id: MenuId.MenubarAppearanceMenu, + group: '3_zoom', + order: 1 + } + }); } - override async run(): Promise { - this.setConfiguredZoomLevel(getZoomLevel() + 1); + override run(accessor: ServicesAccessor): Promise { + return super.setConfiguredZoomLevel(accessor, getZoomLevel() + 1); } } export class ZoomOutAction extends BaseZoomAction { - static readonly ID = 'workbench.action.zoomOut'; - static readonly LABEL = localize('zoomOut', "Zoom Out"); - - constructor( - id: string, - label: string, - @IConfigurationService configurationService: IConfigurationService - ) { - super(id, label, configurationService); + constructor() { + super({ + id: 'workbench.action.zoomOut', + title: { + value: localize('zoomOut', "Zoom Out"), + mnemonicTitle: localize({ key: 'miZoomOut', comment: ['&& denotes a mnemonic'] }, "&&Zoom Out"), + original: 'Zoom Out' + }, + category: CATEGORIES.View.value, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.US_MINUS, + secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_MINUS, KeyMod.CtrlCmd | KeyCode.NUMPAD_SUBTRACT], + linux: { + primary: KeyMod.CtrlCmd | KeyCode.US_MINUS, + secondary: [KeyMod.CtrlCmd | KeyCode.NUMPAD_SUBTRACT] + } + }, + menu: { + id: MenuId.MenubarAppearanceMenu, + group: '3_zoom', + order: 2 + } + }); } - override async run(): Promise { - this.setConfiguredZoomLevel(getZoomLevel() - 1); + override run(accessor: ServicesAccessor): Promise { + return super.setConfiguredZoomLevel(accessor, getZoomLevel() - 1); } } export class ZoomResetAction extends BaseZoomAction { - static readonly ID = 'workbench.action.zoomReset'; - static readonly LABEL = localize('zoomReset', "Reset Zoom"); - - constructor( - id: string, - label: string, - @IConfigurationService configurationService: IConfigurationService - ) { - super(id, label, configurationService); + constructor() { + super({ + id: 'workbench.action.zoomReset', + title: { + value: localize('zoomReset', "Reset Zoom"), + mnemonicTitle: localize({ key: 'miZoomReset', comment: ['&& denotes a mnemonic'] }, "&&Reset Zoom"), + original: 'Reset Zoom' + }, + category: CATEGORIES.View.value, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.NUMPAD_0 + }, + menu: { + id: MenuId.MenubarAppearanceMenu, + group: '3_zoom', + order: 3 + } + }); } - override async run(): Promise { - this.setConfiguredZoomLevel(0); + override run(accessor: ServicesAccessor): Promise { + return super.setConfiguredZoomLevel(accessor, 0); } } -export abstract class BaseSwitchWindow extends Action { +abstract class BaseSwitchWindow extends Action2 { private readonly closeWindowAction: IQuickInputButton = { iconClass: Codicon.removeClose.classNames, @@ -135,24 +189,22 @@ export abstract class BaseSwitchWindow extends Action { alwaysVisible: true }; - constructor( - id: string, - label: string, - private readonly quickInputService: IQuickInputService, - private readonly keybindingService: IKeybindingService, - private readonly modelService: IModelService, - private readonly modeService: IModeService, - private readonly nativeHostService: INativeHostService - ) { - super(id, label); + constructor(desc: Readonly) { + super(desc); } protected abstract isQuickNavigate(): boolean; - override async run(): Promise { - const currentWindowId = this.nativeHostService.windowId; + override async run(accessor: ServicesAccessor): Promise { + const quickInputService = accessor.get(IQuickInputService); + const keybindingService = accessor.get(IKeybindingService); + const modelService = accessor.get(IModelService); + const modeService = accessor.get(IModeService); + const nativeHostService = accessor.get(INativeHostService); - const windows = await this.nativeHostService.getWindows(); + const currentWindowId = nativeHostService.windowId; + + const windows = await nativeHostService.getWindows(); const placeHolder = localize('switchWindowPlaceHolder', "Select a window to switch to"); const picks = windows.map(window => { const resource = window.filename ? URI.file(window.filename) : isSingleFolderWorkspaceIdentifier(window.workspace) ? window.workspace.uri : isWorkspaceIdentifier(window.workspace) ? window.workspace.configPath : undefined; @@ -161,45 +213,43 @@ export abstract class BaseSwitchWindow extends Action { payload: window.id, label: window.title, ariaLabel: window.dirty ? localize('windowDirtyAriaLabel', "{0}, dirty window", window.title) : window.title, - iconClasses: getIconClasses(this.modelService, this.modeService, resource, fileKind), + iconClasses: getIconClasses(modelService, modeService, resource, fileKind), description: (currentWindowId === window.id) ? localize('current', "Current Window") : undefined, buttons: currentWindowId !== window.id ? window.dirty ? [this.closeDirtyWindowAction] : [this.closeWindowAction] : undefined }; }); const autoFocusIndex = (picks.indexOf(picks.filter(pick => pick.payload === currentWindowId)[0]) + 1) % picks.length; - const pick = await this.quickInputService.pick(picks, { + const pick = await quickInputService.pick(picks, { contextKey: 'inWindowsPicker', activeItem: picks[autoFocusIndex], placeHolder, - quickNavigate: this.isQuickNavigate() ? { keybindings: this.keybindingService.lookupKeybindings(this.id) } : undefined, + quickNavigate: this.isQuickNavigate() ? { keybindings: keybindingService.lookupKeybindings(this.desc.id) } : undefined, onDidTriggerItemButton: async context => { - await this.nativeHostService.closeWindowById(context.item.payload); + await nativeHostService.closeWindowById(context.item.payload); context.removeItem(); } }); if (pick) { - this.nativeHostService.focusWindow({ windowId: pick.payload }); + nativeHostService.focusWindow({ windowId: pick.payload }); } } } -export class SwitchWindow extends BaseSwitchWindow { +export class SwitchWindowAction extends BaseSwitchWindow { - static readonly ID = 'workbench.action.switchWindow'; - static readonly LABEL = localize('switchWindow', "Switch Window..."); - - constructor( - id: string, - label: string, - @IQuickInputService quickInputService: IQuickInputService, - @IKeybindingService keybindingService: IKeybindingService, - @IModelService modelService: IModelService, - @IModeService modeService: IModeService, - @INativeHostService nativeHostService: INativeHostService - ) { - super(id, label, quickInputService, keybindingService, modelService, modeService, nativeHostService); + constructor() { + super({ + id: 'workbench.action.switchWindow', + title: { value: localize('switchWindow', "Switch Window..."), original: 'Switch Window...' }, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: 0, + mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_W } + } + }); } protected isQuickNavigate(): boolean { @@ -207,21 +257,14 @@ export class SwitchWindow extends BaseSwitchWindow { } } -export class QuickSwitchWindow extends BaseSwitchWindow { +export class QuickSwitchWindowAction extends BaseSwitchWindow { - static readonly ID = 'workbench.action.quickSwitchWindow'; - static readonly LABEL = localize('quickSwitchWindow', "Quick Switch Window..."); - - constructor( - id: string, - label: string, - @IQuickInputService quickInputService: IQuickInputService, - @IKeybindingService keybindingService: IKeybindingService, - @IModelService modelService: IModelService, - @IModeService modeService: IModeService, - @INativeHostService nativeHostService: INativeHostService - ) { - super(id, label, quickInputService, keybindingService, modelService, modeService, nativeHostService); + constructor() { + super({ + id: 'workbench.action.quickSwitchWindow', + title: { value: localize('quickSwitchWindow', "Quick Switch Window..."), original: 'Quick Switch Window...' }, + f1: true + }); } protected isQuickNavigate(): boolean { diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index fa19dd0342..c17c680852 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -6,22 +6,24 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { localize } from 'vs/nls'; import product from 'vs/platform/product/common/product'; -import { SyncActionDescriptor, MenuRegistry, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { MenuRegistry, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { IWorkbenchActionRegistry, Extensions, CATEGORIES } from 'vs/workbench/common/actions'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { isLinux, isMacintosh } from 'vs/base/common/platform'; import { ConfigureRuntimeArgumentsAction, ToggleDevToolsAction, ToggleSharedProcessAction, ReloadWindowWithExtensionsDisabledAction } from 'vs/workbench/electron-sandbox/actions/developerActions'; -import { ZoomResetAction, ZoomOutAction, ZoomInAction, CloseCurrentWindowAction, SwitchWindow, QuickSwitchWindow, NewWindowTabHandler, ShowPreviousWindowTabHandler, ShowNextWindowTabHandler, MoveWindowTabToNewWindowHandler, MergeWindowTabsHandlerHandler, ToggleWindowTabsBarHandler } from 'vs/workbench/electron-sandbox/actions/windowActions'; +import { ZoomResetAction, ZoomOutAction, ZoomInAction, CloseWindowAction, SwitchWindowAction, QuickSwitchWindowAction, NewWindowTabHandler, ShowPreviousWindowTabHandler, ShowNextWindowTabHandler, MoveWindowTabToNewWindowHandler, MergeWindowTabsHandlerHandler, ToggleWindowTabsBarHandler } from 'vs/workbench/electron-sandbox/actions/windowActions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IsMacContext } from 'vs/platform/contextkey/common/contextkeys'; -import { EditorsVisibleContext, SingleEditorGroupsContext } from 'vs/workbench/common/editor'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { PartsSplash } from 'vs/workbench/electron-sandbox/splash'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { InstallShellScriptAction, UninstallShellScriptAction } from 'vs/workbench/electron-sandbox/actions/installActions'; // eslint-disable-next-line code-import-patterns import { SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; @@ -29,82 +31,60 @@ import * as locConstants from 'sql/base/common/locConstants'; // {{SQL CARBON ED // Actions (function registerActions(): void { - const registry = Registry.as(Extensions.WorkbenchActions); // Actions: Zoom - (function registerZoomActions(): void { - registry.registerWorkbenchAction(SyncActionDescriptor.from(ZoomInAction, { primary: KeyMod.CtrlCmd | KeyCode.US_EQUAL, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_EQUAL, KeyMod.CtrlCmd | KeyCode.NUMPAD_ADD] }), 'View: Zoom In', CATEGORIES.View.value); - registry.registerWorkbenchAction(SyncActionDescriptor.from(ZoomOutAction, { primary: KeyMod.CtrlCmd | KeyCode.US_MINUS, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_MINUS, KeyMod.CtrlCmd | KeyCode.NUMPAD_SUBTRACT], linux: { primary: KeyMod.CtrlCmd | KeyCode.US_MINUS, secondary: [KeyMod.CtrlCmd | KeyCode.NUMPAD_SUBTRACT] } }), 'View: Zoom Out', CATEGORIES.View.value); - registry.registerWorkbenchAction(SyncActionDescriptor.from(ZoomResetAction, { primary: KeyMod.CtrlCmd | KeyCode.NUMPAD_0 }), 'View: Reset Zoom', CATEGORIES.View.value); - })(); + registerAction2(ZoomInAction); + registerAction2(ZoomOutAction); + registerAction2(ZoomResetAction); // Actions: Window - (function registerWindowActions(): void { - registry.registerWorkbenchAction(SyncActionDescriptor.from(SwitchWindow, { primary: 0, mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_W } }), 'Switch Window...'); - registry.registerWorkbenchAction(SyncActionDescriptor.from(QuickSwitchWindow), 'Quick Switch Window...'); + registerAction2(SwitchWindowAction); + registerAction2(QuickSwitchWindowAction); + registerAction2(CloseWindowAction); - // Close window - registry.registerWorkbenchAction(SyncActionDescriptor.from(CloseCurrentWindowAction, { - mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_W }, - linux: { primary: KeyMod.Alt | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_W] }, - win: { primary: KeyMod.Alt | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_W] } - } - ), 'Close Window'); + // Actions: Install Shell Script (macOS only) + if (isMacintosh) { + registerAction2(InstallShellScriptAction); + registerAction2(UninstallShellScriptAction); + } - // Close the window when the last editor is closed by reusing the same keybinding - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: CloseCurrentWindowAction.ID, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(EditorsVisibleContext.toNegated(), SingleEditorGroupsContext), - primary: KeyMod.CtrlCmd | KeyCode.KEY_W, - handler: accessor => { - const nativeHostService = accessor.get(INativeHostService); - nativeHostService.closeWindow(); - } - }); - - // Quit - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'workbench.action.quit', - weight: KeybindingWeight.WorkbenchContrib, - handler(accessor: ServicesAccessor) { - const nativeHostService = accessor.get(INativeHostService); - nativeHostService.quit(); - }, - when: undefined, - mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_Q }, - linux: { primary: KeyMod.CtrlCmd | KeyCode.KEY_Q } - }); - })(); + // Quit + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.action.quit', + weight: KeybindingWeight.WorkbenchContrib, + handler(accessor: ServicesAccessor) { + const nativeHostService = accessor.get(INativeHostService); + nativeHostService.quit(); + }, + when: undefined, + mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_Q }, + linux: { primary: KeyMod.CtrlCmd | KeyCode.KEY_Q } + }); // Actions: macOS Native Tabs - (function registerMacOSNativeTabsActions(): void { - if (isMacintosh) { - [ - { handler: NewWindowTabHandler, id: 'workbench.action.newWindowTab', title: { value: localize('newTab', "New Window Tab"), original: 'New Window Tab' } }, - { handler: ShowPreviousWindowTabHandler, id: 'workbench.action.showPreviousWindowTab', title: { value: localize('showPreviousTab', "Show Previous Window Tab"), original: 'Show Previous Window Tab' } }, - { handler: ShowNextWindowTabHandler, id: 'workbench.action.showNextWindowTab', title: { value: localize('showNextWindowTab', "Show Next Window Tab"), original: 'Show Next Window Tab' } }, - { handler: MoveWindowTabToNewWindowHandler, id: 'workbench.action.moveWindowTabToNewWindow', title: { value: localize('moveWindowTabToNewWindow', "Move Window Tab to New Window"), original: 'Move Window Tab to New Window' } }, - { handler: MergeWindowTabsHandlerHandler, id: 'workbench.action.mergeAllWindowTabs', title: { value: localize('mergeAllWindowTabs', "Merge All Windows"), original: 'Merge All Windows' } }, - { handler: ToggleWindowTabsBarHandler, id: 'workbench.action.toggleWindowTabsBar', title: { value: localize('toggleWindowTabsBar', "Toggle Window Tabs Bar"), original: 'Toggle Window Tabs Bar' } } - ].forEach(command => { - CommandsRegistry.registerCommand(command.id, command.handler); + if (isMacintosh) { + [ + { handler: NewWindowTabHandler, id: 'workbench.action.newWindowTab', title: { value: localize('newTab', "New Window Tab"), original: 'New Window Tab' } }, + { handler: ShowPreviousWindowTabHandler, id: 'workbench.action.showPreviousWindowTab', title: { value: localize('showPreviousTab', "Show Previous Window Tab"), original: 'Show Previous Window Tab' } }, + { handler: ShowNextWindowTabHandler, id: 'workbench.action.showNextWindowTab', title: { value: localize('showNextWindowTab', "Show Next Window Tab"), original: 'Show Next Window Tab' } }, + { handler: MoveWindowTabToNewWindowHandler, id: 'workbench.action.moveWindowTabToNewWindow', title: { value: localize('moveWindowTabToNewWindow', "Move Window Tab to New Window"), original: 'Move Window Tab to New Window' } }, + { handler: MergeWindowTabsHandlerHandler, id: 'workbench.action.mergeAllWindowTabs', title: { value: localize('mergeAllWindowTabs', "Merge All Windows"), original: 'Merge All Windows' } }, + { handler: ToggleWindowTabsBarHandler, id: 'workbench.action.toggleWindowTabsBar', title: { value: localize('toggleWindowTabsBar', "Toggle Window Tabs Bar"), original: 'Toggle Window Tabs Bar' } } + ].forEach(command => { + CommandsRegistry.registerCommand(command.id, command.handler); - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command, - when: ContextKeyExpr.equals('config.window.nativeTabs', true) - }); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command, + when: ContextKeyExpr.equals('config.window.nativeTabs', true) }); - } - })(); + }); + } // Actions: Developer - (function registerDeveloperActions(): void { - registerAction2(ReloadWindowWithExtensionsDisabledAction); - registerAction2(ConfigureRuntimeArgumentsAction); - registerAction2(ToggleSharedProcessAction); - registerAction2(ToggleDevToolsAction); - })(); + registerAction2(ReloadWindowWithExtensionsDisabledAction); + registerAction2(ConfigureRuntimeArgumentsAction); + registerAction2(ToggleSharedProcessAction); + registerAction2(ToggleDevToolsAction); })(); // Menu @@ -120,15 +100,6 @@ import * as locConstants from 'sql/base/common/locConstants'; // {{SQL CARBON ED }); } - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: '6_close', - command: { - id: CloseCurrentWindowAction.ID, - title: localize({ key: 'miCloseWindow', comment: ['&& denotes a mnemonic'] }, "Clos&&e Window") - }, - order: 4 - }); - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { group: 'z_Exit', command: { @@ -138,56 +109,6 @@ import * as locConstants from 'sql/base/common/locConstants'; // {{SQL CARBON ED order: 1, when: IsMacContext.toNegated() }); - - // Zoom - - MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '3_zoom', - command: { - id: ZoomInAction.ID, - title: localize({ key: 'miZoomIn', comment: ['&& denotes a mnemonic'] }, "&&Zoom In") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '3_zoom', - command: { - id: ZoomOutAction.ID, - title: localize({ key: 'miZoomOut', comment: ['&& denotes a mnemonic'] }, "&&Zoom Out") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '3_zoom', - command: { - id: ZoomResetAction.ID, - title: localize({ key: 'miZoomReset', comment: ['&& denotes a mnemonic'] }, "&&Reset Zoom") - }, - order: 3 - }); - - if (!!product.reportIssueUrl) { - MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: '3_feedback', - command: { - id: 'workbench.action.openIssueReporter', - title: localize({ key: 'miReportIssue', comment: ['&& denotes a mnemonic', 'Translate this to "Report Issue in English" in all languages please!'] }, "Report &&Issue") - }, - order: 3 - }); - } - - // Tools - MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: '5_tools', - command: { - id: 'workbench.action.openProcessExplorer', - title: localize({ key: 'miOpenProcessExplorerer', comment: ['&& denotes a mnemonic'] }, "Open &&Process Explorer") - }, - order: 2 - }); })(); // Configuration @@ -401,3 +322,10 @@ import * as locConstants from 'sql/base/common/locConstants'; // {{SQL CARBON ED jsonRegistry.registerSchema(argvDefinitionFileSchemaId, schema); })(); + +// Workbench Contributions +(function registerWorkbenchContributions() { + + // Splash + Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(PartsSplash, LifecyclePhase.Starting); +})(); diff --git a/src/vs/workbench/electron-sandbox/parts/dialogs/dialogHandler.ts b/src/vs/workbench/electron-sandbox/parts/dialogs/dialogHandler.ts index cfa0db2755..81d05de2da 100644 --- a/src/vs/workbench/electron-sandbox/parts/dialogs/dialogHandler.ts +++ b/src/vs/workbench/electron-sandbox/parts/dialogs/dialogHandler.ts @@ -90,7 +90,7 @@ export class NativeDialogHandler implements IDialogHandler { return opts; } - async show(severity: Severity, message: string, buttons: string[], dialogOptions?: IDialogOptions): Promise { + async show(severity: Severity, message: string, buttons?: string[], dialogOptions?: IDialogOptions): Promise { this.logService.trace('DialogService#show', message); const { options, buttonIndexMap } = this.massageMessageBoxOptions({ @@ -167,7 +167,7 @@ export class NativeDialogHandler implements IDialogHandler { const detailString = (useAgo: boolean): string => { return localize({ key: 'aboutDetail', comment: ['Electron, Chrome, Node.js and V8 are product names that need no translation'] }, - "Version: {0}\nCommit: {1}\nDate: {2}\nVS Code: {8}\nElectron: {3}\nChrome: {4}\nNode.js: {5}\nV8: {6}\nOS: {7}", + "Version: {0}\nCommit: {1}\nDate: {2}\nVS Code: {8}\nElectron: {3}\nChrome: {4}\nNode.js: {5}\nV8: {6}\nOS: {7}", // {{SQL CARBON EDIT}} version, this.productService.commit || 'Unknown', this.productService.date ? `${this.productService.date}${useAgo ? ' (' + fromNow(new Date(this.productService.date), true) + ')' : ''}` : 'Unknown', diff --git a/src/vs/workbench/electron-sandbox/parts/titlebar/menubarControl.ts b/src/vs/workbench/electron-sandbox/parts/titlebar/menubarControl.ts index b9b57857e8..7522cbfa0d 100644 --- a/src/vs/workbench/electron-sandbox/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/electron-sandbox/parts/titlebar/menubarControl.ts @@ -3,9 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import { Separator } from 'vs/base/common/actions'; -import { IMenuService, MenuId, IMenu, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { IMenuService, IMenu, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { isMacintosh } from 'vs/base/common/platform'; @@ -48,11 +47,6 @@ export class NativeMenubarControl extends MenubarControl { ) { super(menuService, workspacesService, contextKeyService, keybindingService, configurationService, labelService, updateService, storageService, notificationService, preferencesService, environmentService, accessibilityService, hostService, commandService); - if (isMacintosh) { - this.menus['Preferences'] = this._register(this.menuService.createMenu(MenuId.MenubarPreferencesMenu, this.contextKeyService)); - this.topLevelTitles['Preferences'] = localize('mPreferences', "Preferences"); - } - for (const topLevelMenuName of Object.keys(this.topLevelTitles)) { const menu = this.menus[topLevelMenuName]; if (menu) { diff --git a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts index e2f52f196a..d2b9e63bb3 100644 --- a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts +++ b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts @@ -11,14 +11,11 @@ import { Event } from 'vs/base/common/event'; import { IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IExtensionService, NullExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IProcessEnvironment, isWindows, OperatingSystem } from 'vs/base/common/platform'; -import { IWebviewService, WebviewContentOptions, WebviewElement, WebviewExtensionDescription, WebviewOptions, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; +import { isWindows } from 'vs/base/common/platform'; import { ITunnelProvider, ITunnelService, RemoteTunnel, TunnelProviderFeatures } from 'vs/platform/remote/common/tunnel'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { joinPath } from 'vs/base/common/resources'; import { VSBuffer } from 'vs/base/common/buffer'; -import { TerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminalInstanceService'; -import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { SearchService } from 'vs/workbench/services/search/common/searchService'; import { ISearchService } from 'vs/workbench/services/search/common/search'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -28,8 +25,6 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; -import { IShellLaunchConfigResolveOptions, ITerminalProfile, ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; -import { IShellLaunchConfig } from 'vs/platform/terminal/common/terminal'; //#region Environment @@ -268,23 +263,6 @@ registerSingleton(IExtensionService, SimpleExtensionService); //#endregion -//#region Webview - -class SimpleWebviewService implements IWebviewService { - declare readonly _serviceBrand: undefined; - - readonly activeWebview = undefined; - readonly onDidChangeActiveWebview = Event.None; - - createWebviewElement(id: string, options: WebviewOptions, contentOptions: WebviewContentOptions, extension: WebviewExtensionDescription | undefined): WebviewElement { throw new Error('Method not implemented.'); } - createWebviewOverlay(id: string, options: WebviewOptions, contentOptions: WebviewContentOptions, extension: WebviewExtensionDescription | undefined): WebviewOverlay { throw new Error('Method not implemented.'); } -} - -registerSingleton(IWebviewService, SimpleWebviewService); - -//#endregion - - //#region Tunnel class SimpleTunnelService implements ITunnelService { @@ -296,6 +274,7 @@ class SimpleTunnelService implements ITunnelService { canMakePublic = false; onTunnelOpened = Event.None; onTunnelClosed = Event.None; + hasTunnelProvider = false; canTunnel(uri: URI): boolean { return false; } openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number): Promise | undefined { return undefined; } @@ -309,34 +288,6 @@ registerSingleton(ITunnelService, SimpleTunnelService); //#endregion -//#region Terminal Instance - -class SimpleTerminalInstanceService extends TerminalInstanceService { } - -registerSingleton(ITerminalInstanceService, SimpleTerminalInstanceService); - -//#endregion - - -//#region Terminal Profile Resolver Service - -class SimpleTerminalProfileResolverService implements ITerminalProfileResolverService { - - _serviceBrand: undefined; - - resolveIcon(shellLaunchConfig: IShellLaunchConfig, os: OperatingSystem): void { } - async resolveShellLaunchConfig(shellLaunchConfig: IShellLaunchConfig, options: IShellLaunchConfigResolveOptions): Promise { } - getDefaultProfile(options: IShellLaunchConfigResolveOptions): Promise { throw new Error('Method not implemented.'); } - getDefaultShell(options: IShellLaunchConfigResolveOptions): Promise { throw new Error('Method not implemented.'); } - getDefaultShellArgs(options: IShellLaunchConfigResolveOptions): Promise { throw new Error('Method not implemented.'); } - getShellEnvironment(remoteAuthority: string | undefined): Promise { throw new Error('Method not implemented.'); } - getSafeConfigValue(key: string, os: OperatingSystem): unknown | undefined { throw new Error('Method not implemented.'); } - getSafeConfigValueFullKey(key: string): unknown | undefined { throw new Error('Method not implemented.'); } -} - -registerSingleton(ITerminalProfileResolverService, SimpleTerminalProfileResolverService); - - //#region Search Service class SimpleSearchService extends SearchService { diff --git a/src/vs/workbench/electron-sandbox/shared.desktop.main.ts b/src/vs/workbench/electron-sandbox/shared.desktop.main.ts index fa857a32a3..e264ed6bfc 100644 --- a/src/vs/workbench/electron-sandbox/shared.desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/shared.desktop.main.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import product from 'vs/platform/product/common/product'; @@ -208,7 +208,7 @@ export abstract class SharedDesktopMain extends Disposable { await result; } - // Uri Identity + // URI Identity const uriIdentityService = new UriIdentityService(fileService); serviceCollection.set(IUriIdentityService, uriIdentityService); @@ -264,7 +264,7 @@ export abstract class SharedDesktopMain extends Disposable { ]); // Workspace Trust Service - const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, environmentService, storageService, uriIdentityService, configurationService); + const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, storageService, uriIdentityService, environmentService, configurationService, remoteAuthorityResolverService); serviceCollection.set(IWorkspaceTrustManagementService, workspaceTrustManagementService); // Update workspace trust so that configuration is updated accordingly diff --git a/src/vs/workbench/electron-sandbox/splash.ts b/src/vs/workbench/electron-sandbox/splash.ts new file mode 100644 index 0000000000..7088a1acae --- /dev/null +++ b/src/vs/workbench/electron-sandbox/splash.ts @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { onDidChangeFullscreen, isFullscreen } from 'vs/base/browser/browser'; +import { getTotalHeight, getTotalWidth } from 'vs/base/browser/dom'; +import { Color } from 'vs/base/common/color'; +import { Event } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { editorBackground, foreground } from 'vs/platform/theme/common/colorRegistry'; +import { getThemeTypeSelector, IThemeService } from 'vs/platform/theme/common/themeService'; +import { DEFAULT_EDITOR_MIN_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; +import * as themes from 'vs/workbench/common/theme'; +import { IWorkbenchLayoutService, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import * as perf from 'vs/base/common/performance'; +import { assertIsDefined } from 'vs/base/common/types'; +import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; + +export class PartsSplash { + + private static readonly _splashElementId = 'monaco-parts-splash'; + + private readonly _disposables = new DisposableStore(); + + private _didChangeTitleBarStyle?: boolean; + + constructor( + @IThemeService private readonly _themeService: IThemeService, + @IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, + @ILifecycleService lifecycleService: ILifecycleService, + @IEditorGroupsService editorGroupsService: IEditorGroupsService, + @IConfigurationService configService: IConfigurationService, + @INativeHostService private readonly _nativeHostService: INativeHostService + ) { + lifecycleService.when(LifecyclePhase.Restored).then(_ => { + this._removePartsSplash(); + perf.mark('code/didRemovePartsSplash'); + }); + + Event.debounce(Event.any( + onDidChangeFullscreen, + editorGroupsService.onDidLayout + ), () => { }, 800)(this._savePartsSplash, this, this._disposables); + + configService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('window.titleBarStyle')) { + this._didChangeTitleBarStyle = true; + this._savePartsSplash(); + } + }, this, this._disposables); + + _themeService.onDidColorThemeChange(_ => { + this._savePartsSplash(); + }, this, this._disposables); + } + + private _savePartsSplash() { + const theme = this._themeService.getColorTheme(); + + this._nativeHostService.saveWindowSplash({ + baseTheme: getThemeTypeSelector(theme.type), + colorInfo: { + foreground: theme.getColor(foreground)?.toString(), + background: Color.Format.CSS.formatHex(theme.getColor(editorBackground) || themes.WORKBENCH_BACKGROUND(theme)), + editorBackground: theme.getColor(editorBackground)?.toString(), + titleBarBackground: theme.getColor(themes.TITLE_BAR_ACTIVE_BACKGROUND)?.toString(), + activityBarBackground: theme.getColor(themes.ACTIVITY_BAR_BACKGROUND)?.toString(), + sideBarBackground: theme.getColor(themes.SIDE_BAR_BACKGROUND)?.toString(), + statusBarBackground: theme.getColor(themes.STATUS_BAR_BACKGROUND)?.toString(), + statusBarNoFolderBackground: theme.getColor(themes.STATUS_BAR_NO_FOLDER_BACKGROUND)?.toString(), + windowBorder: theme.getColor(themes.WINDOW_ACTIVE_BORDER)?.toString() ?? theme.getColor(themes.WINDOW_INACTIVE_BORDER)?.toString() + }, + layoutInfo: !this._shouldSaveLayoutInfo() ? undefined : { + sideBarSide: this._layoutService.getSideBarPosition() === Position.RIGHT ? 'right' : 'left', + editorPartMinWidth: DEFAULT_EDITOR_MIN_DIMENSIONS.width, + titleBarHeight: this._layoutService.isVisible(Parts.TITLEBAR_PART) ? getTotalHeight(assertIsDefined(this._layoutService.getContainer(Parts.TITLEBAR_PART))) : 0, + activityBarWidth: this._layoutService.isVisible(Parts.ACTIVITYBAR_PART) ? getTotalWidth(assertIsDefined(this._layoutService.getContainer(Parts.ACTIVITYBAR_PART))) : 0, + sideBarWidth: this._layoutService.isVisible(Parts.SIDEBAR_PART) ? getTotalWidth(assertIsDefined(this._layoutService.getContainer(Parts.SIDEBAR_PART))) : 0, + statusBarHeight: this._layoutService.isVisible(Parts.STATUSBAR_PART) ? getTotalHeight(assertIsDefined(this._layoutService.getContainer(Parts.STATUSBAR_PART))) : 0, + windowBorder: this._layoutService.hasWindowBorder(), + windowBorderRadius: this._layoutService.getWindowBorderRadius() + } + }); + } + + private _shouldSaveLayoutInfo(): boolean { + return !isFullscreen() && !this._environmentService.isExtensionDevelopment && !this._didChangeTitleBarStyle; + } + + private _removePartsSplash(): void { + const element = document.getElementById(PartsSplash._splashElementId); + if (element) { + element.style.display = 'none'; + } + + // remove initial colors + const defaultStyles = document.head.getElementsByClassName('initialShellColors'); + if (defaultStyles.length) { + document.head.removeChild(defaultStyles[0]); + } + } + + dispose(): void { + this._disposables.dispose(); + } +} diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index a04782cc7e..c3e4fbe7c2 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -10,7 +10,7 @@ import { equals } from 'vs/base/common/objects'; import { EventType, EventHelper, addDisposableListener, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; import { Separator } from 'vs/base/common/actions'; import { IFileService } from 'vs/platform/files/common/files'; -import { EditorResourceAccessor, IUntitledTextResourceEditorInput, SideBySideEditor, pathsToEditors } from 'vs/workbench/common/editor'; +import { EditorResourceAccessor, IUntitledTextResourceEditorInput, SideBySideEditor, pathsToEditors, IResourceDiffEditorInput } from 'vs/workbench/common/editor'; import { IEditorService, IResourceEditorInputType } from 'vs/workbench/services/editor/common/editorService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WindowMinimumSize, IOpenFileRequest, IWindowsConfiguration, getTitleBarStyle, IAddFoldersRequest, INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, INativeOpenFileRequest } from 'vs/platform/windows/common/windows'; @@ -19,7 +19,7 @@ import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/work import { applyZoom } from 'vs/platform/windows/electron-sandbox/window'; import { setFullscreen, getZoomLevel } from 'vs/base/browser/browser'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { IBaseResourceEditorInput, IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { env } from 'vs/base/common/process'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; @@ -32,7 +32,7 @@ import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/work import { IIntegrityService } from 'vs/workbench/services/integrity/common/integrity'; import { isWindows, isMacintosh } from 'vs/base/common/platform'; import { IProductService } from 'vs/platform/product/common/productService'; -import { INotificationService, IPromptChoice, NeverShowAgainScope, Severity } from 'vs/platform/notification/common/notification'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; @@ -59,7 +59,7 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { AuthInfo } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes'; import { ILogService } from 'vs/platform/log/common/log'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { whenTextEditorClosed } from 'vs/workbench/browser/editor'; +import { whenEditorClosed } from 'vs/workbench/browser/editor'; export class NativeWindow extends Disposable { @@ -186,38 +186,9 @@ export class NativeWindow extends Disposable { // Message support ipcRenderer.on('vscode:showInfoMessage', (event: unknown, message: string) => this.notificationService.info(message)); - // Shell Environment Issue Notifications - const choices: IPromptChoice[] = [{ - label: localize('learnMore', "Learn More"), - run: () => this.openerService.open('https://go.microsoft.com/fwlink/?linkid=2149667') - }]; - - ipcRenderer.on('vscode:showShellEnvSlowWarning', () => this.notificationService.prompt( - Severity.Warning, - localize('shellEnvSlowWarning', "Resolving your shell environment is taking very long. Please review your shell configuration."), - choices, - { - sticky: true, - neverShowAgain: { id: 'ignoreShellEnvSlowWarning', scope: NeverShowAgainScope.GLOBAL } - } - )); - - ipcRenderer.on('vscode:showShellEnvTimeoutError', () => this.notificationService.prompt( - Severity.Error, - localize('shellEnvTimeoutError', "Unable to resolve your shell environment in a reasonable time. Please review your shell configuration."), - choices - )); - // Fullscreen Events - ipcRenderer.on('vscode:enterFullScreen', async () => { - await this.lifecycleService.when(LifecyclePhase.Ready); - setFullscreen(true); - }); - - ipcRenderer.on('vscode:leaveFullScreen', async () => { - await this.lifecycleService.when(LifecyclePhase.Ready); - setFullscreen(false); - }); + ipcRenderer.on('vscode:enterFullScreen', async () => setFullscreen(true)); + ipcRenderer.on('vscode:leaveFullScreen', async () => setFullscreen(false)); // Proxy Login Dialog ipcRenderer.on('vscode:openProxyAuthenticationDialog', async (event: unknown, payload: { authInfo: AuthInfo, username?: string, password?: string, replyChannel: string }) => { @@ -495,7 +466,7 @@ export class NativeWindow extends Disposable { this.logService.error('Error: There is a dependency cycle in the AMD modules that needs to be resolved!'); this.nativeHostService.exit(37); // running on a build machine, just exit without showing a dialog } else { - this.dialogService.show(Severity.Error, localize('loaderCycle', "There is a dependency cycle in the AMD modules that needs to be resolved!"), [localize('ok', "OK")]); + this.dialogService.show(Severity.Error, localize('loaderCycle', "There is a dependency cycle in the AMD modules that needs to be resolved!")); this.nativeHostService.openDevTools(); } } @@ -547,6 +518,21 @@ export class NativeWindow extends Disposable { } } } + + // Assume `uri` this is a workspace uri, let's see if we can handle it + await this.fileService.activateProvider(uri.scheme); + + if (this.fileService.canHandleResource(uri)) { + return { + resolved: URI.from({ + scheme: this.productService.urlProtocol, + path: 'workspace', + query: uri.toString() + }), + dispose() { } + }; + } + return undefined; } }); @@ -662,33 +648,35 @@ export class NativeWindow extends Disposable { // In wait mode, listen to changes to the editors and wait until the files // are closed that the user wants to wait for. When this happens we delete // the wait marker file to signal to the outside that editing is done. - this.trackClosedWaitFiles(URI.revive(request.filesToWait.waitMarkerFileUri), coalesce(request.filesToWait.paths.map(p => URI.revive(p.fileUri)))); + this.trackClosedWaitFiles(URI.revive(request.filesToWait.waitMarkerFileUri), coalesce(request.filesToWait.paths.map(path => URI.revive(path.fileUri)))); } } private async trackClosedWaitFiles(waitMarkerFile: URI, resourcesToWaitFor: URI[]): Promise { // Wait for the resources to be closed in the text editor... - await this.instantiationService.invokeFunction(accessor => whenTextEditorClosed(accessor, resourcesToWaitFor)); + await this.instantiationService.invokeFunction(accessor => whenEditorClosed(accessor, resourcesToWaitFor)); // ...before deleting the wait marker file await this.fileService.del(waitMarkerFile); } private async openResources(resources: Array, diffMode: boolean): Promise { - await this.lifecycleService.when(LifecyclePhase.Ready); + const editors: IBaseResourceEditorInput[] = []; // In diffMode we open 2 resources as diff if (diffMode && resources.length === 2 && resources[0].resource && resources[1].resource) { - return this.editorService.openEditor({ leftResource: resources[0].resource, rightResource: resources[1].resource, options: { pinned: true } }); + const diffEditor: IResourceDiffEditorInput = { + originalInput: { resource: resources[0].resource }, + modifiedInput: { resource: resources[1].resource }, + options: { pinned: true } + }; + editors.push(diffEditor); + } else { + editors.push(...resources); } - // For one file, just put it into the current active editor - if (resources.length === 1) { - return this.editorService.openEditor(resources[0]); - } - - // Otherwise open all - return this.editorService.openEditors(resources); + // Open as editors + return this.editorService.openEditors(editors, undefined, { validateTrust: true }); } } diff --git a/src/vs/workbench/services/activity/common/activity.ts b/src/vs/workbench/services/activity/common/activity.ts index dc71731947..17060cf502 100644 --- a/src/vs/workbench/services/activity/common/activity.ts +++ b/src/vs/workbench/services/activity/common/activity.ts @@ -46,7 +46,7 @@ export interface IBadge { class BaseBadge implements IBadge { - constructor(public readonly descriptorFn: (arg: any) => string) { + constructor(readonly descriptorFn: (arg: any) => string) { this.descriptorFn = descriptorFn; } @@ -57,7 +57,7 @@ class BaseBadge implements IBadge { export class NumberBadge extends BaseBadge { - constructor(public readonly number: number, descriptorFn: (num: number) => string) { + constructor(readonly number: number, descriptorFn: (num: number) => string) { super(descriptorFn); this.number = number; @@ -70,13 +70,13 @@ export class NumberBadge extends BaseBadge { export class TextBadge extends BaseBadge { - constructor(public readonly text: string, descriptorFn: () => string) { + constructor(readonly text: string, descriptorFn: () => string) { super(descriptorFn); } } export class IconBadge extends BaseBadge { - constructor(public readonly icon: ThemeIcon, descriptorFn: () => string) { + constructor(readonly icon: ThemeIcon, descriptorFn: () => string) { super(descriptorFn); } } diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index 10d72a3c9c..eac313f21a 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -37,7 +37,7 @@ export interface IAccountUsage { lastUsed: number; } -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', 'ms-vscode.remotehub', 'ms-vscode.remotehub-insiders', 'github.codespaces']; +const VSO_ALLOWED_EXTENSIONS = ['github.vscode-pull-request-github', 'github.vscode-pull-request-github-insiders', 'vscode.git', 'ms-vsonline.vsonline', 'ms-vscode.remotehub', 'ms-vscode.remotehub-insiders', 'github.remotehub', 'github.remotehub-insiders', 'github.codespaces']; export function readAccountUsages(storageService: IStorageService, providerId: string, accountName: string,): IAccountUsage[] { const accountKey = `${providerId}-${accountName}-usages`; @@ -338,7 +338,7 @@ export class AuthenticationService extends Disposable implements IAuthentication } Object.keys(existingRequestsForProvider).forEach(requestedScopes => { - if (addedSessions.some(session => session.scopes.slice().sort().join('') === requestedScopes)) { + if (addedSessions.some(session => session.scopes.slice().join('') === requestedScopes)) { const sessionRequest = existingRequestsForProvider[requestedScopes]; sessionRequest?.disposables.forEach(item => item.dispose()); @@ -565,9 +565,11 @@ export class AuthenticationService extends Disposable implements IAuthentication id: `${providerId}${extensionId}Access`, title: nls.localize({ key: 'accessRequest', - comment: ['The placeholder {0} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count'] + comment: [`The placeholder {0} will be replaced with an authentication provider''s label. {1} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count`] }, - "Grant access to {0}... (1)", extensionName) + "Grant access to {0} for {1}... (1)", + this.getLabel(providerId), + extensionName) } }); @@ -602,7 +604,7 @@ export class AuthenticationService extends Disposable implements IAuthentication if (provider) { const providerRequests = this._signInRequestItems.get(providerId); - const scopesList = scopes.sort().join(''); + const scopesList = scopes.join(''); const extensionHasExistingRequest = providerRequests && providerRequests[scopesList] && providerRequests[scopesList].requestingExtensionIds.includes(extensionId); @@ -615,12 +617,12 @@ export class AuthenticationService extends Disposable implements IAuthentication group: '2_signInRequests', command: { id: `${extensionId}signIn`, - title: nls.localize( - { - key: 'signInRequest', - comment: ['The placeholder {0} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count.'] - }, - "Sign in to use {0} (1)", + title: nls.localize({ + key: 'signInRequest', + comment: [`The placeholder {0} will be replaced with an authentication provider's label. {1} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count.`] + }, + "Sign in with {0} to use {1} (1)", + provider.label, extensionName) } }); diff --git a/src/vs/workbench/services/banner/browser/bannerService.ts b/src/vs/workbench/services/banner/browser/bannerService.ts new file mode 100644 index 0000000000..834d062251 --- /dev/null +++ b/src/vs/workbench/services/banner/browser/bannerService.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from 'vs/base/common/codicons'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ILinkDescriptor } from 'vs/platform/opener/browser/link'; + + +export interface IBannerItem { + readonly id: string; + readonly icon: Codicon; + readonly message: string | MarkdownString; + readonly actions?: ILinkDescriptor[]; + readonly ariaLabel?: string; + readonly onClose?: () => void; +} + +export const IBannerService = createDecorator('bannerService'); + +export interface IBannerService { + readonly _serviceBrand: undefined; + + focus(): void; + focusNextAction(): void; + focusPreviousAction(): void; + hide(id: string): void; + show(item: IBannerItem): void; +} diff --git a/src/vs/workbench/services/clipboard/browser/clipboardService.ts b/src/vs/workbench/services/clipboard/browser/clipboardService.ts index afcadce8de..5bcf4e3e92 100644 --- a/src/vs/workbench/services/clipboard/browser/clipboardService.ts +++ b/src/vs/workbench/services/clipboard/browser/clipboardService.ts @@ -12,13 +12,16 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { once } from 'vs/base/common/functional'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { isSafari } from 'vs/base/browser/browser'; +import { ILogService } from 'vs/platform/log/common/log'; export class BrowserClipboardService extends BaseBrowserClipboardService { constructor( @INotificationService private readonly notificationService: INotificationService, @IOpenerService private readonly openerService: IOpenerService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @ILogService private readonly logService: ILogService ) { super(); } @@ -35,6 +38,12 @@ export class BrowserClipboardService extends BaseBrowserClipboardService { return ''; // do not ask for input in tests (https://github.com/microsoft/vscode/issues/112264) } + if (isSafari) { + this.logService.error(error); + + return ''; // Safari does not seem to provide anyway to enable cipboard access (https://github.com/microsoft/vscode-internalbacklog/issues/2162#issuecomment-852042867) + } + return new Promise(resolve => { // Inform user about permissions problem (https://github.com/microsoft/vscode/issues/112089) diff --git a/src/vs/workbench/services/configuration/common/configuration.ts b/src/vs/workbench/services/configuration/common/configuration.ts index 9644666a27..1a2461c272 100644 --- a/src/vs/workbench/services/configuration/common/configuration.ts +++ b/src/vs/workbench/services/configuration/common/configuration.ts @@ -3,13 +3,12 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; import { ResourceMap } from 'vs/base/common/map'; -import { Registry } from 'vs/platform/registry/common/platform'; // {{SQL CARBON EDIT}} export const FOLDER_CONFIG_FOLDER_NAME = '.azuredatastudio'; @@ -50,14 +49,6 @@ export interface IConfigurationCache { } -export function filterSettingsRequireWorkspaceTrust(settings: ReadonlyArray): ReadonlyArray { - const configurationRegistry = Registry.as(Extensions.Configuration); - return settings.filter(key => { - const property = configurationRegistry.getConfigurationProperties()[key]; - return property.restricted && property.scope !== ConfigurationScope.APPLICATION && property.scope !== ConfigurationScope.MACHINE; - }); -} - export type RestrictedSettings = { default: ReadonlyArray; userLocal?: ReadonlyArray; diff --git a/src/vs/workbench/services/configuration/test/common/testServices.ts b/src/vs/workbench/services/configuration/test/common/testServices.ts index 3771f39ba6..179930b36c 100644 --- a/src/vs/workbench/services/configuration/test/common/testServices.ts +++ b/src/vs/workbench/services/configuration/test/common/testServices.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts index b763da1724..d3016d83a8 100644 --- a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts @@ -19,6 +19,7 @@ import { IQuickInputService, IInputOptions, IQuickPickItem, IPickOptions } from import { ConfiguredInput } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { ILabelService } from 'vs/platform/label/common/label'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; export abstract class BaseConfigurationResolverService extends AbstractVariableResolverService { @@ -35,7 +36,8 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR private readonly commandService: ICommandService, private readonly workspaceContextService: IWorkspaceContextService, private readonly quickInputService: IQuickInputService, - private readonly labelService: ILabelService + private readonly labelService: ILabelService, + private readonly pathService: IPathService ) { super({ getFolderUri: (folderName: string): uri | undefined => { @@ -57,7 +59,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR getFilePath: (): string | undefined => { const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY, - filterByScheme: [Schemas.file, Schemas.userData, Schemas.vscodeRemote] + filterByScheme: [Schemas.file, Schemas.userData, this.pathService.defaultUriScheme] }); if (!fileResource) { return undefined; @@ -67,7 +69,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR getWorkspaceFolderPathForFile: (): string | undefined => { const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY, - filterByScheme: [Schemas.file, Schemas.userData, Schemas.vscodeRemote] + filterByScheme: [Schemas.file, Schemas.userData, this.pathService.defaultUriScheme] }); if (!fileResource) { return undefined; @@ -366,9 +368,10 @@ export class ConfigurationResolverService extends BaseConfigurationResolverServi @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @IQuickInputService quickInputService: IQuickInputService, @ILabelService labelService: ILabelService, + @IPathService pathService: IPathService ) { super({ getAppRoot: () => undefined, getExecPath: () => undefined }, Promise.resolve(Object.create(null)), editorService, configurationService, - commandService, workspaceContextService, quickInputService, labelService); + commandService, workspaceContextService, quickInputService, labelService, pathService); } } diff --git a/src/vs/workbench/services/configurationResolver/common/configurationResolverUtils.ts b/src/vs/workbench/services/configurationResolver/common/configurationResolverUtils.ts index bcf5dea6b9..922a8921d5 100644 --- a/src/vs/workbench/services/configurationResolver/common/configurationResolverUtils.ts +++ b/src/vs/workbench/services/configurationResolver/common/configurationResolverUtils.ts @@ -9,4 +9,4 @@ export function applyDeprecatedVariableMessage(schema: IJSONSchema) { schema.pattern = schema.pattern || '^(?!.*\\$\\{(env|config|command)\\.)'; schema.patternErrorMessage = schema.patternErrorMessage || nls.localize('deprecatedVariables', "'env.', 'config.' and 'command.' are deprecated, use 'env:', 'config:' and 'command:' instead."); -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/configurationResolver/common/variableResolver.ts b/src/vs/workbench/services/configurationResolver/common/variableResolver.ts index 51bab9107c..1a9f1a4eaa 100644 --- a/src/vs/workbench/services/configurationResolver/common/variableResolver.ts +++ b/src/vs/workbench/services/configurationResolver/common/variableResolver.ts @@ -141,7 +141,7 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe // loop through all variables occurrences in 'value' const replaced = value.replace(AbstractVariableResolverService.VARIABLE_REGEXP, (match: string, variable: string) => { - // disallow attempted nesting, see #77289 + // disallow attempted nesting, see #77289. This doesn't exclude variables that resolve to other variables. if (variable.includes(AbstractVariableResolverService.VARIABLE_LHS)) { return match; } @@ -152,6 +152,10 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe resolvedVariables.set(variable, resolvedValue); } + if ((resolvedValue !== match) && types.isString(resolvedValue) && resolvedValue.match(AbstractVariableResolverService.VARIABLE_REGEXP)) { + resolvedValue = this.resolveString(environment, folderUri, resolvedValue, commandValueMapping, resolvedVariables); + } + return resolvedValue; }); diff --git a/src/vs/workbench/services/configurationResolver/electron-sandbox/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/electron-sandbox/configurationResolverService.ts index 2b0bccda64..3f093daa16 100644 --- a/src/vs/workbench/services/configurationResolver/electron-sandbox/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/electron-sandbox/configurationResolverService.ts @@ -14,6 +14,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; import { ILabelService } from 'vs/platform/label/common/label'; import { IShellEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/shellEnvironmentService'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; export class ConfigurationResolverService extends BaseConfigurationResolverService { @@ -25,7 +26,8 @@ export class ConfigurationResolverService extends BaseConfigurationResolverServi @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @IQuickInputService quickInputService: IQuickInputService, @ILabelService labelService: ILabelService, - @IShellEnvironmentService shellEnvironmentService: IShellEnvironmentService + @IShellEnvironmentService shellEnvironmentService: IShellEnvironmentService, + @IPathService pathService: IPathService ) { super({ getAppRoot: (): string | undefined => { @@ -34,7 +36,8 @@ export class ConfigurationResolverService extends BaseConfigurationResolverServi getExecPath: (): string | undefined => { return environmentService.execPath; } - }, shellEnvironmentService.getShellEnv(), editorService, configurationService, commandService, workspaceContextService, quickInputService, labelService); + }, shellEnvironmentService.getShellEnv(), editorService, configurationService, commandService, + workspaceContextService, quickInputService, labelService, pathService); } } diff --git a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts index 6fea0cba12..9e190f5892 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts @@ -6,7 +6,8 @@ import * as assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { normalize } from 'vs/base/common/path'; +import { Schemas } from 'vs/base/common/network'; +import { IPath, normalize } from 'vs/base/common/path'; import * as platform from 'vs/base/common/platform'; import { isObject } from 'vs/base/common/types'; import { URI as uri } from 'vs/base/common/uri'; @@ -22,6 +23,7 @@ import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { TestEditorService, TestProductService, TestQuickInputService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestWorkbenchConfiguration } from 'vs/workbench/test/electron-browser/workbenchTestServices'; @@ -66,6 +68,7 @@ suite('Configuration Resolver Service', () => { let workspace: IWorkspaceFolder; let quickInputService: TestQuickInputService; let labelService: MockLabelService; + let pathService: MockPathService; setup(() => { mockCommandService = new MockCommandService(); @@ -73,9 +76,10 @@ suite('Configuration Resolver Service', () => { quickInputService = new TestQuickInputService(); environmentService = new MockWorkbenchEnvironmentService(envVariables); labelService = new MockLabelService(); + pathService = new MockPathService(); containingWorkspace = testWorkspace(uri.parse('file:///VSCode/workspaceLocation')); workspace = containingWorkspace.folders[0]; - configurationResolverService = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), editorService, new MockInputsConfigurationService(), mockCommandService, new TestContextService(containingWorkspace), quickInputService, labelService); + configurationResolverService = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), editorService, new MockInputsConfigurationService(), mockCommandService, new TestContextService(containingWorkspace), quickInputService, labelService, pathService); }); teardown(() => { @@ -214,7 +218,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} xyz'), 'abc foo xyz'); }); @@ -225,7 +229,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); assert.strictEqual(await service.resolveAsync(undefined, 'abc ${config:editor.fontFamily} xyz'), 'abc foo xyz'); }); @@ -242,7 +246,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} xyz'), 'abc foo bar xyz'); }); @@ -259,7 +263,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); if (platform.isWindows) { assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} ${workspaceFolder} ${env:key1} xyz'), 'abc foo \\VSCode\\workspaceLocation Value for key1 xyz'); } else { @@ -280,7 +284,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); if (platform.isWindows) { assert.strictEqual(await service.resolveAsync(workspace, '${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} ${workspaceFolder} - ${workspaceFolder} ${env:key1} - ${env:key2}'), 'foo bar \\VSCode\\workspaceLocation - \\VSCode\\workspaceLocation Value for key1 - Value for key2'); } else { @@ -314,7 +318,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} ${config:editor.lineNumbers} ${config:editor.insertSpaces} xyz'), 'abc foo 123 false xyz'); }); @@ -324,7 +328,7 @@ suite('Configuration Resolver Service', () => { editor: {} }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); assert.strictEqual(await service.resolveAsync(workspace, 'abc ${unknownVariable} xyz'), 'abc ${unknownVariable} xyz'); assert.strictEqual(await service.resolveAsync(workspace, 'abc ${env:unknownVariable} xyz'), 'abc xyz'); }); @@ -337,7 +341,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); assert.rejects(async () => await service.resolveAsync(workspace, 'abc ${env} xyz')); assert.rejects(async () => await service.resolveAsync(workspace, 'abc ${env:} xyz')); @@ -675,6 +679,21 @@ class MockLabelService implements ILabelService { onDidChangeFormatters: Event = new Emitter().event; } +class MockPathService implements IPathService { + _serviceBrand: undefined; + get path(): Promise { + throw new Error('Property not implemented'); + } + defaultUriScheme: string = Schemas.file; + fileURI(path: string): Promise { + throw new Error('Method not implemented.'); + } + userHome(options?: { preferLocal: boolean; }): Promise { + throw new Error('Method not implemented.'); + } + resolvedUserHome: uri | undefined; +} + class MockInputsConfigurationService extends TestConfigurationService { public override getValue(arg1?: any, arg2?: any): any { let configuration; diff --git a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts index dd432e4161..9aadde5dd4 100644 --- a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts +++ b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts @@ -25,6 +25,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { stripIcons } from 'vs/base/common/iconLabels'; import { coalesce } from 'vs/base/common/arrays'; +import { Emitter } from 'vs/base/common/event'; export class ContextMenuService extends Disposable implements IContextMenuService { @@ -32,6 +33,9 @@ export class ContextMenuService extends Disposable implements IContextMenuServic private impl: IContextMenuService; + private readonly _onDidShowContextMenu = this._register(new Emitter()); + readonly onDidShowContextMenu = this._onDidShowContextMenu.event; + constructor( @INotificationService notificationService: INotificationService, @ITelemetryService telemetryService: ITelemetryService, @@ -56,6 +60,7 @@ export class ContextMenuService extends Disposable implements IContextMenuServic showContextMenu(delegate: IContextMenuDelegate): void { this.impl.showContextMenu(delegate); + this._onDidShowContextMenu.fire(); } } @@ -63,6 +68,8 @@ class NativeContextMenuService extends Disposable implements IContextMenuService declare readonly _serviceBrand: undefined; + readonly onDidShowContextMenu = new Emitter().event; + constructor( @INotificationService private readonly notificationService: INotificationService, @ITelemetryService private readonly telemetryService: ITelemetryService, diff --git a/src/vs/workbench/services/credentials/browser/credentialsService.ts b/src/vs/workbench/services/credentials/browser/credentialsService.ts index 1bc22e4ca7..9b7bcd31b2 100644 --- a/src/vs/workbench/services/credentials/browser/credentialsService.ts +++ b/src/vs/workbench/services/credentials/browser/credentialsService.ts @@ -38,9 +38,11 @@ export class BrowserCredentialsService extends Disposable implements ICredential this._onDidChangePassword.fire({ service, account }); } - deletePassword(service: string, account: string): Promise { - const didDelete = this.credentialsProvider.deletePassword(service, account); - this._onDidChangePassword.fire({ service, account }); + async deletePassword(service: string, account: string): Promise { + const didDelete = await this.credentialsProvider.deletePassword(service, account); + if (didDelete) { + this._onDidChangePassword.fire({ service, account }); + } return didDelete; } diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index 00ac31ccf5..5e188490ac 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -75,12 +75,9 @@ class DecorationRule { const { color, letter } = data; // label createCSSRule(`.${this.itemColorClassName}`, `color: ${getColor(theme, color)};`, element); - // icon if (ThemeIcon.isThemeIcon(letter)) { this._createIconCSSRule(letter, color, element, theme); - } - // letter - else if (letter) { + } else if (letter) { createCSSRule(`.${this.itemBadgeClassName}::after`, `content: "${letter}"; color: ${getColor(theme, color)};`, element); } } @@ -93,6 +90,7 @@ class DecorationRule { // icon (only show first) const icon = data.find(d => ThemeIcon.isThemeIcon(d.letter))?.letter as ThemeIcon | undefined; if (icon) { + // todo@jrieken this is fishy. icons should be just like letter and not mute bubble badge this._createIconCSSRule(icon, color, element, theme); } else { // badge @@ -112,14 +110,26 @@ class DecorationRule { } private _createIconCSSRule(icon: ThemeIcon, color: string | undefined, element: HTMLStyleElement, theme: IColorTheme) { - const codicon = iconRegistry.get(icon.id); + + const index = icon.id.lastIndexOf('~'); + const id = index < 0 ? icon.id : icon.id.substr(0, index); + const modifier = index < 0 ? '' : icon.id.substr(index + 1); + + const codicon = iconRegistry.get(id); if (!codicon || !('fontCharacter' in codicon.definition)) { return; } const charCode = parseInt(codicon.definition.fontCharacter.substr(1), 16); createCSSRule( `.${this.iconBadgeClassName}::after`, - `content: "${String.fromCharCode(charCode)}"; color: ${getColor(theme, color)}; font-family: codicon; font-size: 16px; padding-right: 14px; font-weight: normal`, + `content: "${String.fromCharCode(charCode)}"; + color: ${getColor(theme, color)}; + font-family: codicon; + font-size: 16px; + padding-right: 14px; + font-weight: normal; + ${modifier === 'spin' ? 'animation: codicon-spin 1.5s steps(30) infinite' : ''}; + `, element ); } diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index a2f8b9d032..28351780fa 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -450,8 +450,12 @@ export class SimpleFileDialog { private constructFullUserPath(): string { const currentFolderPath = this.pathFromUri(this.currentFolder); - if (equalsIgnoreCase(this.filePickBox.value.substr(0, this.userEnteredPathSegment.length), this.userEnteredPathSegment) && equalsIgnoreCase(this.filePickBox.value.substr(0, currentFolderPath.length), currentFolderPath)) { - return currentFolderPath; + if (equalsIgnoreCase(this.filePickBox.value.substr(0, this.userEnteredPathSegment.length), this.userEnteredPathSegment)) { + if (equalsIgnoreCase(this.filePickBox.value.substr(0, currentFolderPath.length), currentFolderPath)) { + return currentFolderPath; + } else { + return this.userEnteredPathSegment; + } } else { return this.pathAppend(this.currentFolder, this.userEnteredPathSegment); } diff --git a/src/vs/workbench/services/dialogs/common/dialogService.ts b/src/vs/workbench/services/dialogs/common/dialogService.ts index 0a2ccdd397..137fd2f7bf 100644 --- a/src/vs/workbench/services/dialogs/common/dialogService.ts +++ b/src/vs/workbench/services/dialogs/common/dialogService.ts @@ -6,26 +6,30 @@ import Severity from 'vs/base/common/severity'; import { Disposable } from 'vs/base/common/lifecycle'; import { IConfirmation, IConfirmationResult, IDialogOptions, IDialogService, IInput, IInputResult, IShowResult } from 'vs/platform/dialogs/common/dialogs'; -import { DialogsModel, IDialogsModel } from 'vs/workbench/common/dialogs'; +import { DialogsModel } from 'vs/workbench/common/dialogs'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; export class DialogService extends Disposable implements IDialogService { - _serviceBrand: undefined; - readonly model: IDialogsModel = this._register(new DialogsModel()); + declare readonly _serviceBrand: undefined; + + readonly model = this._register(new DialogsModel()); async confirm(confirmation: IConfirmation): Promise { const handle = this.model.show({ confirmArgs: { confirmation } }); + return await handle.result as IConfirmationResult; } - async show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise { + async show(severity: Severity, message: string, buttons?: string[], options?: IDialogOptions): Promise { const handle = this.model.show({ showArgs: { severity, message, buttons, options } }); + return await handle.result as IShowResult; } async input(severity: Severity, message: string, buttons: string[], inputs: IInput[], options?: IDialogOptions): Promise { const handle = this.model.show({ inputArgs: { severity, message, buttons, inputs, options } }); + return await handle.result as IInputResult; } diff --git a/src/vs/workbench/services/editor/browser/codeEditorService.ts b/src/vs/workbench/services/editor/browser/codeEditorService.ts index 5c28a49a40..3ad335a2ec 100644 --- a/src/vs/workbench/services/editor/browser/codeEditorService.ts +++ b/src/vs/workbench/services/editor/browser/codeEditorService.ts @@ -8,12 +8,13 @@ import { CodeEditorServiceImpl } from 'vs/editor/browser/services/codeEditorServ import { ScrollType } from 'vs/editor/common/editorCommon'; import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IWorkbenchEditorConfiguration, TextEditorOptions } from 'vs/workbench/common/editor'; +import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { isEqual } from 'vs/base/common/resources'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions'; export class CodeEditorService extends CodeEditorServiceImpl { @@ -60,8 +61,7 @@ export class CodeEditorService extends CodeEditorServiceImpl { ) { const targetEditor = activeTextEditorControl.getModifiedEditor(); - const textOptions = TextEditorOptions.create(input.options); - textOptions.apply(targetEditor, ScrollType.Smooth); + applyTextEditorOptions(input.options, targetEditor, ScrollType.Smooth); return targetEditor; } diff --git a/src/vs/workbench/services/editor/browser/editorOverrideService.ts b/src/vs/workbench/services/editor/browser/editorOverrideService.ts index 5f91483a16..26b88a9f07 100644 --- a/src/vs/workbench/services/editor/browser/editorOverrideService.ts +++ b/src/vs/workbench/services/editor/browser/editorOverrideService.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as glob from 'vs/base/common/glob'; @@ -9,15 +9,14 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle' import { basename, extname, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { EditorActivation, EditorOverride, IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; -import { EditorResourceAccessor, IEditorInput, IEditorInputWithOptions, IEditorInputWithOptionsAndGroup } from 'vs/workbench/common/editor'; +import { EditorActivation, EditorOverride, IEditorOptions } from 'vs/platform/editor/common/editor'; +import { EditorResourceAccessor, IEditorInput, IEditorInputWithOptions, IEditorInputWithOptionsAndGroup, SideBySideEditor } from 'vs/workbench/common/editor'; import { IEditorGroup, IEditorGroupsService, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; import { Schemas } from 'vs/base/common/network'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; -import { ContributedEditorInfo, ContributedEditorPriority, ContributionPointOptions, DEFAULT_EDITOR_ASSOCIATION, DiffEditorInputFactoryFunction, EditorAssociation, EditorAssociations, EditorInputFactoryFunction, editorsAssociationsSettingId, globMatchesResource, IEditorOverrideService, priorityToRank } from 'vs/workbench/services/editor/common/editorOverrideService'; +import { ContributedEditorInfo, ContributedEditorPriority, RegisteredEditorOptions, DEFAULT_EDITOR_ASSOCIATION, DiffEditorInputFactoryFunction, EditorAssociation, EditorAssociations, EditorInputFactoryFunction, editorsAssociationsSettingId, globMatchesResource, IEditorOverrideService, priorityToRank } from 'vs/workbench/services/editor/common/editorOverrideService'; import { IKeyMods, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { localize } from 'vs/nls'; -import { Codicon } from 'vs/base/common/codicons'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -28,21 +27,26 @@ interface IContributedEditorInput extends IEditorInput { viewType?: string; } -interface ContributionPoint { +interface RegisteredEditor { globPattern: string | glob.IRelativePattern, editorInfo: ContributedEditorInfo, - options?: ContributionPointOptions, + options?: RegisteredEditorOptions, createEditorInput: EditorInputFactoryFunction createDiffEditorInput?: DiffEditorInputFactoryFunction } -type ContributionPoints = Array; +type RegisteredEditors = Array; export class EditorOverrideService extends Disposable implements IEditorOverrideService { readonly _serviceBrand: undefined; - private _contributionPoints: Map = new Map(); + // Constants + private static readonly configureDefaultID = 'promptOpenWith.configureDefault'; private static readonly overrideCacheStorageID = 'editorOverrideService.cache'; + private static readonly conflictingDefaultsStorageID = 'editorOverrideService.conflictingDefaults'; + + // Data Stores + private _editors: Map = new Map(); // private cache: Set | undefined; {{SQL CARBON EDIT}} Remove unused constructor( @@ -58,10 +62,11 @@ export class EditorOverrideService extends Disposable implements IEditorOverride // Read in the cache on statup // this.cache = new Set(JSON.parse(this.storageService.get(EditorOverrideService.overrideCacheStorageID, StorageScope.GLOBAL, JSON.stringify([])))); {{SQL CARBON EDIT}} Remove unused this.storageService.remove(EditorOverrideService.overrideCacheStorageID, StorageScope.GLOBAL); + this.convertOldAssociationFormat(); this._register(this.storageService.onWillSaveState(() => { // We want to store the glob patterns we would activate on, this allows us to know if we need to await the ext host on startup for opening a resource - this.cacheContributionPoints(); + this.cacheEditors(); })); // When extensions have registered we no longer need the cache @@ -70,9 +75,14 @@ export class EditorOverrideService extends Disposable implements IEditorOverride this.cache = undefined; }); */ + + // When the setting changes we want to ensure that it is properly converted + this._register(this.configurationService.onDidChangeConfiguration(() => { + this.convertOldAssociationFormat(); + })); } - async resolveEditorOverride(editor: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup): Promise { + async resolveEditorOverride(editor: IEditorInput, options: IEditorOptions | undefined, group: IEditorGroup): Promise { // If it was an override before we await for the extensions to activate and then proceed with overriding or else they won't be registered //if (this.cache && editor.resource && this.resourceMatchesCache(editor.resource)) { // {{SQL CARBON EDIT}} Always wait for extensions so that our language-based overrides (SQL/Notebooks) will always have those registered await this.extensionService.whenInstalledExtensionsRegistered(); @@ -83,11 +93,8 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } // Always ensure inputs have populated resource fields - if (editor instanceof DiffEditorInput) { - if ((!editor.modifiedInput.resource || !editor.originalInput.resource)) { - return { editor, options, group }; - } - } else if (!editor.resource) { + const resource = EditorResourceAccessor.getCanonicalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY }); + if (!resource) { return { editor, options, group }; } @@ -107,29 +114,28 @@ export class EditorOverrideService extends Disposable implements IEditorOverride group = picked[1] ?? group; } - // Resolved the override as much as possible, now find a given contribution - const { contributionPoint, conflictingDefault } = this.getContributionPoint(editor instanceof DiffEditorInput ? editor.modifiedInput.resource! : editor.resource!, override); - const selectedContribution = contributionPoint; - if (!selectedContribution) { + // Resolved the override as much as possible, now find a given editor + const { editor: matchededEditor, conflictingDefault } = this.getEditor(resource, override); + const selectedEditor = matchededEditor; + if (!selectedEditor) { return { editor, options, group }; } - const handlesDiff = typeof selectedContribution.options?.canHandleDiff === 'function' ? selectedContribution.options.canHandleDiff() : selectedContribution.options?.canHandleDiff; + const handlesDiff = typeof selectedEditor.options?.canHandleDiff === 'function' ? selectedEditor.options.canHandleDiff() : selectedEditor.options?.canHandleDiff; if (editor instanceof DiffEditorInput && handlesDiff === false) { return { editor, options, group }; } // If it's the currently active editor we shouldn't do anything - if (selectedContribution.editorInfo.describes(editor)) { + if (selectedEditor.editorInfo.describes(editor)) { return undefined; // {{SQL CARBON EDIT}} Strict nulls } - const input = await this.doOverrideEditorInput(editor, options, group, selectedContribution); + const input = await this.doOverrideEditorInput(resource, editor, options, group, selectedEditor); if (conflictingDefault && input) { - // Wait one second to give the user ample time to see the current editor then ask them to configure a default - setTimeout(() => { - this.doHandleConflictingDefaults(selectedContribution.editorInfo.label, input.editor, input.options ?? options, group); - }, 1200); + // Show the conflicting default dialog + await this.doHandleConflictingDefaults(resource, selectedEditor.editorInfo.label, input.editor, input.options ?? options, group); } + // Add the group as we might've changed it with the quickpick if (input) { this.sendOverrideTelemetry(input.editor); @@ -138,17 +144,19 @@ export class EditorOverrideService extends Disposable implements IEditorOverride return input; } - registerContributionPoint( + registerEditor( globPattern: string | glob.IRelativePattern, editorInfo: ContributedEditorInfo, - options: ContributionPointOptions, + options: RegisteredEditorOptions, createEditorInput: EditorInputFactoryFunction, createDiffEditorInput?: DiffEditorInputFactoryFunction ): IDisposable { - if (this._contributionPoints.get(globPattern) === undefined) { - this._contributionPoints.set(globPattern, []); + let registeredEditor = this._editors.get(globPattern); + if (registeredEditor === undefined) { + registeredEditor = []; + this._editors.set(globPattern, registeredEditor); } - const remove = insert(this._contributionPoints.get(globPattern)!, { + const remove = insert(registeredEditor, { globPattern, editorInfo, options, @@ -158,91 +166,129 @@ export class EditorOverrideService extends Disposable implements IEditorOverride return toDisposable(() => remove()); } - hasContributionPoint(schemeOrGlob: string): boolean { - return this._contributionPoints.has(schemeOrGlob); + getAssociationsForResource(resource: URI): EditorAssociations { + const associations = this.getAllUserAssociations(); + const matchingAssociations = associations.filter(association => association.filenamePattern && globMatchesResource(association.filenamePattern, resource)); + const allEditors: RegisteredEditors = this._registeredEditors; + // Ensure that the settings are valid editors + return matchingAssociations.filter(association => allEditors.find(c => c.editorInfo.id === association.viewType)); } - getAssociationsForResource(resource: URI): EditorAssociations { - const rawAssociations = this.configurationService.getValue(editorsAssociationsSettingId) || []; - return rawAssociations.filter(association => association.filenamePattern && globMatchesResource(association.filenamePattern, resource)); + private convertOldAssociationFormat(): void { + const rawAssociations = this.configurationService.getValue(editorsAssociationsSettingId) || []; + // If it's not an array, then it's the new format + if (!Array.isArray(rawAssociations)) { + return; + } + let newSettingObject = Object.create(null); + // Make the correctly formatted object from the array and then set that object + for (const association of rawAssociations) { + if (association.filenamePattern) { + newSettingObject[association.filenamePattern] = association.viewType; + } + } + this.configurationService.updateValue(editorsAssociationsSettingId, newSettingObject); + } + + private getAllUserAssociations(): EditorAssociations { + const rawAssociations = this.configurationService.getValue<{ [fileNamePattern: string]: string }>(editorsAssociationsSettingId) || []; + let associations = []; + for (const [key, value] of Object.entries(rawAssociations)) { + const association: EditorAssociation = { + filenamePattern: key, + viewType: value + }; + associations.push(association); + } + return associations; + } + + /** + * Returns all editors as an array. Possible to contain duplicates + */ + private get _registeredEditors(): RegisteredEditors { + return flatten(Array.from(this._editors.values())); } updateUserAssociations(globPattern: string, editorID: string): void { const newAssociation: EditorAssociation = { viewType: editorID, filenamePattern: globPattern }; - const currentAssociations = [...this.configurationService.getValue(editorsAssociationsSettingId)]; - - // First try updating existing association - for (let i = 0; i < currentAssociations.length; ++i) { - const existing = currentAssociations[i]; - if (existing.filenamePattern === newAssociation.filenamePattern) { - currentAssociations.splice(i, 1, newAssociation); - this.configurationService.updateValue(editorsAssociationsSettingId, currentAssociations); - return; + const currentAssociations = this.getAllUserAssociations(); + const newSettingObject = Object.create(null); + // Form the new setting object including the newest associations + for (const association of [...currentAssociations, newAssociation]) { + if (association.filenamePattern) { + newSettingObject[association.filenamePattern] = association.viewType; } } - - // Otherwise, create a new one - currentAssociations.unshift(newAssociation); - this.configurationService.updateValue(editorsAssociationsSettingId, currentAssociations); + this.configurationService.updateValue(editorsAssociationsSettingId, newSettingObject); } - private findMatchingContributions(resource: URI): ContributionPoint[] { - let contributions: ContributionPoint[] = []; + private findMatchingEditors(resource: URI): RegisteredEditor[] { + // The user setting should be respected even if the editor doesn't specify that resource in package.json + const userSettings = this.getAssociationsForResource(resource); + let matchingEditors: RegisteredEditor[] = []; // Then all glob patterns - for (const key of this._contributionPoints.keys()) { - const contributionPoints = this._contributionPoints.get(key)!; - for (const contributionPoint of contributionPoints) { - if (globMatchesResource(key, resource)) { - contributions.push(contributionPoint); + for (const [key, editors] of this._editors) { + for (const editor of editors) { + const foundInSettings = userSettings.find(setting => setting.viewType === editor.editorInfo.id); + if (foundInSettings || globMatchesResource(key, resource)) { + matchingEditors.push(editor); } } } - // Return the contributions sorted by their priority - return contributions.sort((a, b) => priorityToRank(b.editorInfo.priority) - priorityToRank(a.editorInfo.priority)); + // Return the editors sorted by their priority + return matchingEditors.sort((a, b) => priorityToRank(b.editorInfo.priority) - priorityToRank(a.editorInfo.priority)); + } + + public getEditorIds(resource: URI): string[] { + const editors = this.findMatchingEditors(resource); + return editors.map(editor => editor.editorInfo.id); } /** - * Given a resource and an override selects the best possible contribution point - * @returns The contribution point and whether there was another default which conflicted with it + * Given a resource and an override selects the best possible editor + * @returns The editor and whether there was another default which conflicted with it */ - private getContributionPoint(resource: URI, override: string | undefined): { contributionPoint: ContributionPoint | undefined, conflictingDefault: boolean } { - const findMatchingContribPoint = (contributionPoints: ContributionPoints, viewType: string) => { - return contributionPoints.find((contribPoint) => { - if (contribPoint.options && contribPoint.options.canSupportResource !== undefined) { - return contribPoint.editorInfo.id === viewType && contribPoint.options.canSupportResource(resource); + private getEditor(resource: URI, override: string | undefined): { editor: RegisteredEditor | undefined, conflictingDefault: boolean } { + const findMatchingEditor = (editors: RegisteredEditors, viewType: string) => { + return editors.find((editor) => { + if (editor.options && editor.options.canSupportResource !== undefined) { + return editor.editorInfo.id === viewType && editor.options.canSupportResource(resource); } - return contribPoint.editorInfo.id === viewType; + return editor.editorInfo.id === viewType; }); }; if (override) { - // Specific overried passed in doesn't have to match the reosurce, it can be anything - const contributionPoints = flatten(Array.from(this._contributionPoints.values())); + // Specific overried passed in doesn't have to match the resource, it can be anything + const registeredEditors = this._registeredEditors; return { - contributionPoint: findMatchingContribPoint(contributionPoints, override), + editor: findMatchingEditor(registeredEditors, override), conflictingDefault: false }; } - let contributionPoints = this.findMatchingContributions(resource); + let editors = this.findMatchingEditors(resource); const associationsFromSetting = this.getAssociationsForResource(resource); // We only want built-in+ if no user defined setting is found, else we won't override - const possibleContributionPoints = contributionPoints.filter(contribPoint => priorityToRank(contribPoint.editorInfo.priority) >= priorityToRank(ContributedEditorPriority.builtin) && contribPoint.editorInfo.id !== DEFAULT_EDITOR_ASSOCIATION.id); - // If the user has a setting we use that, else choose the highest priority editor that is built-in+ - const selectedViewType = associationsFromSetting[0]?.viewType || possibleContributionPoints[0]?.editorInfo.id; + const possibleEditors = editors.filter(editor => priorityToRank(editor.editorInfo.priority) >= priorityToRank(ContributedEditorPriority.builtin) && editor.editorInfo.id !== DEFAULT_EDITOR_ASSOCIATION.id); + // If the editor is exclusive we use that, else use the user setting, else use the built-in+ editor + const selectedViewType = possibleEditors[0]?.editorInfo.priority === ContributedEditorPriority.exclusive ? + possibleEditors[0]?.editorInfo.id : + associationsFromSetting[0]?.viewType || possibleEditors[0]?.editorInfo.id; let conflictingDefault = false; - if (associationsFromSetting.length === 0 && possibleContributionPoints.length > 1) { + if (associationsFromSetting.length === 0 && possibleEditors.length > 1) { conflictingDefault = true; } return { - contributionPoint: findMatchingContribPoint(contributionPoints, selectedViewType), + editor: findMatchingEditor(editors, selectedViewType), conflictingDefault }; } - private async doOverrideEditorInput(editor: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup, selectedContribution: ContributionPoint): Promise { + private async doOverrideEditorInput(resource: URI, editor: IEditorInput, options: IEditorOptions | undefined, group: IEditorGroup, selectedEditor: RegisteredEditor): Promise { // If no activation option is provided, populate it. if (options && typeof options.activation === 'undefined') { @@ -251,25 +297,22 @@ export class EditorOverrideService extends Disposable implements IEditorOverride // If it's a diff editor we trigger the create diff editor input if (editor instanceof DiffEditorInput) { - if (!selectedContribution.createDiffEditorInput) { + if (!selectedEditor.createDiffEditorInput) { return undefined; // {{SQL CARBON EDIT}} Strict nulls } - const inputWithOptions = selectedContribution.createDiffEditorInput(editor, options, group); + const inputWithOptions = selectedEditor.createDiffEditorInput(editor, options, group); return inputWithOptions; } - // We only call this function from one place and there we do the check to ensure editor.resource is not undefined - const resource = editor.resource!; - // Respect options passed back - const inputWithOptions = selectedContribution.createEditorInput(resource, options, group); + const inputWithOptions = selectedEditor.createEditorInput(resource, options, group); options = inputWithOptions.options ?? options; const input = inputWithOptions.editor; // If the editor states it can only be opened once per resource we must close all existing ones first - const singleEditorPerResource = typeof selectedContribution.options?.singlePerResource === 'function' ? selectedContribution.options.singlePerResource() : selectedContribution.options?.singlePerResource; + const singleEditorPerResource = typeof selectedEditor.options?.singlePerResource === 'function' ? selectedEditor.options.singlePerResource() : selectedEditor.options?.singlePerResource; if (singleEditorPerResource) { - this.closeExistingEditorsForResource(resource, selectedContribution.editorInfo.id, group); + this.closeExistingEditorsForResource(resource, selectedEditor.editorInfo.id, group); } return { editor: input, options }; @@ -325,16 +368,27 @@ export class EditorOverrideService extends Disposable implements IEditorOverride return out; } - private async doHandleConflictingDefaults(editorName: string, currentEditor: IContributedEditorInput, options: IEditorOptions | undefined, group: IEditorGroup) { - const makeCurrentEditorDefault = () => { - const viewType = currentEditor.viewType; - if (viewType) { - this.updateUserAssociations(`*${extname(currentEditor.resource!)}`, viewType); - } + private async doHandleConflictingDefaults(resource: URI, editorName: string, currentEditor: IContributedEditorInput, options: IEditorOptions | undefined, group: IEditorGroup) { + type StoredChoice = { + [key: string]: string[]; + }; + const editors = this.findMatchingEditors(resource); + const storedChoices: StoredChoice = JSON.parse(this.storageService.get(EditorOverrideService.conflictingDefaultsStorageID, StorageScope.GLOBAL, '{}')); + const globForResource = `*${extname(resource)}`; + // Writes to the storage service that a choice has been made for the currently installed editors + const writeCurrentEditorsToStorage = () => { + storedChoices[globForResource] = []; + editors.forEach(editor => storedChoices[globForResource].push(editor.editorInfo.id)); + this.storageService.store(EditorOverrideService.conflictingDefaultsStorageID, JSON.stringify(storedChoices), StorageScope.GLOBAL, StorageTarget.MACHINE); }; + // If the user has already made a choice for this editor we don't want to ask them again + if (storedChoices[globForResource] && storedChoices[globForResource].find(editorID => editorID === currentEditor.viewType)) { + return; + } + const handle = this.notificationService.prompt(Severity.Warning, - localize('editorOverride.conflictingDefaults', 'Multiple editors want to be your default editor for this resource.'), + localize('editorOverride.conflictingDefaults', 'There are multiple default editors available for the resource.'), [{ label: localize('editorOverride.configureDefault', 'Configure Default'), run: async () => { @@ -359,23 +413,25 @@ export class EditorOverrideService extends Disposable implements IEditorOverride }, { label: localize('editorOverride.keepDefault', 'Keep {0}', editorName), - run: makeCurrentEditorDefault + run: writeCurrentEditorsToStorage } ]); // If the user pressed X we assume they want to keep the current editor as default const onCloseListener = handle.onDidClose(() => { - makeCurrentEditorDefault(); + writeCurrentEditorsToStorage(); onCloseListener.dispose(); }); } - private mapContributionsToQuickPickEntry(resource: URI, group: IEditorGroup, alwaysUpdateSetting?: boolean) { + private mapEditorsToQuickPickEntry(resource: URI, group: IEditorGroup, showDefaultPicker?: boolean) { const currentEditor = firstOrDefault(group.findEditors(resource)); - // If untitled, we want all contribution points - let contributionPoints = resource.scheme === Schemas.untitled ? distinct(flatten(Array.from(this._contributionPoints.values())), (contrib) => contrib.editorInfo.id) : this.findMatchingContributions(resource); - + // If untitled, we want all registered editors + let registeredEditors = resource.scheme === Schemas.untitled ? this._registeredEditors : this.findMatchingEditors(resource); + // We don't want duplicate Id entries + registeredEditors = distinct(registeredEditors, c => c.editorInfo.id); + const defaultSetting = this.getAssociationsForResource(resource)[0]?.viewType; // Not the most efficient way to do this, but we want to ensure the text editor is at the top of the quickpick - contributionPoints = contributionPoints.sort((a, b) => { + registeredEditors = registeredEditors.sort((a, b) => { if (a.editorInfo.id === DEFAULT_EDITOR_ASSOCIATION.id) { return -1; } else if (b.editorInfo.id === DEFAULT_EDITOR_ASSOCIATION.id) { @@ -384,37 +440,43 @@ export class EditorOverrideService extends Disposable implements IEditorOverride return priorityToRank(b.editorInfo.priority) - priorityToRank(a.editorInfo.priority); } }); - const contribGroups: { defaults: Array, optional: Array } = { - defaults: [ - { type: 'separator', label: localize('editorOverride.picker.default', 'Defaults') } - ], - optional: [ - { type: 'separator', label: localize('editorOverride.picker.optional', 'Optional') } - ], - }; - // Get the matching contribtuions and call resolve whether they're active for the picker - contributionPoints.forEach(contribPoint => { - const isActive = currentEditor ? contribPoint.editorInfo.describes(currentEditor) : false; - const quickPickEntry = { - id: contribPoint.editorInfo.id, - label: contribPoint.editorInfo.label, - description: isActive ? localize('promptOpenWith.currentlyActive', "Currently Active") : undefined, - detail: contribPoint.editorInfo.detail ?? contribPoint.editorInfo.priority, - buttons: alwaysUpdateSetting ? [] : [{ - iconClass: Codicon.gear.classNames, - tooltip: localize('promptOpenWith.setDefaultTooltip', "Set as default editor for '{0}' files", extname(resource)) - }], + const quickPickEntries: Array = []; + const currentlyActiveLabel = localize('promptOpenWith.currentlyActive', "Active"); + const currentDefaultLabel = localize('promptOpenWith.currentDefault', "Default"); + const currentDefaultAndActiveLabel = localize('promptOpenWith.currentDefaultAndActive', "Active and Default"); + // Default order = setting -> highest priority -> text + let defaultViewType = defaultSetting; + if (!defaultViewType && registeredEditors.length > 2 && registeredEditors[1]?.editorInfo.priority !== ContributedEditorPriority.option) { + defaultViewType = registeredEditors[1]?.editorInfo.id; + } + if (!defaultViewType) { + defaultViewType = DEFAULT_EDITOR_ASSOCIATION.id; + } + // Map the editors to quickpick entries + registeredEditors.forEach(editor => { + const isActive = currentEditor ? editor.editorInfo.describes(currentEditor) : false; + const isDefault = editor.editorInfo.id === defaultViewType; + const quickPickEntry: IQuickPickItem = { + id: editor.editorInfo.id, + label: editor.editorInfo.label, + description: isActive && isDefault ? currentDefaultAndActiveLabel : isActive ? currentlyActiveLabel : isDefault ? currentDefaultLabel : undefined, + detail: editor.editorInfo.detail ?? editor.editorInfo.priority, }; - if (contribPoint.editorInfo.priority === ContributedEditorPriority.option) { - contribGroups.optional.push(quickPickEntry); - } else { - contribGroups.defaults.push(quickPickEntry); - } + quickPickEntries.push(quickPickEntry); }); - return [...contribGroups.defaults, ...contribGroups.optional]; + if (!showDefaultPicker) { + const separator: IQuickPickSeparator = { type: 'separator' }; + quickPickEntries.push(separator); + const configureDefaultEntry = { + id: EditorOverrideService.configureDefaultID, + label: localize('promptOpenWith.configureDefault', "Configure default editor for '{0}'...", `*${extname(resource)}`), + }; + quickPickEntries.push(configureDefaultEntry); + } + return quickPickEntries; } - private async doPickEditorOverride(editor: IEditorInput, options: IEditorOptions | undefined, group: IEditorGroup, alwaysUpdateSetting?: boolean): Promise<[IEditorOptions, IEditorGroup | undefined] | undefined> { + private async doPickEditorOverride(editor: IEditorInput, options: IEditorOptions | undefined, group: IEditorGroup, showDefaultPicker?: boolean): Promise<[IEditorOptions, IEditorGroup | undefined] | undefined> { type EditorOverridePick = { readonly item: IQuickPickItem; @@ -429,12 +491,12 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } // Text editor has the lowest priority because we - const editorOverridePicks = this.mapContributionsToQuickPickEntry(resource, group, alwaysUpdateSetting); + const editorOverridePicks = this.mapEditorsToQuickPickEntry(resource, group, showDefaultPicker); // Create editor override picker const editorOverridePicker = this.quickInputService.createQuickPick(); - const placeHolderMessage = alwaysUpdateSetting ? - localize('prompOpenWith.updateDefaultPlaceHolder', "Select new default editor for '{0}'", basename(resource)) : + const placeHolderMessage = showDefaultPicker ? + localize('prompOpenWith.updateDefaultPlaceHolder', "Select new default editor for '{0}'", `*${extname(resource)}`) : localize('promptOpenWith.placeHolder', "Select editor for '{0}'", basename(resource)); editorOverridePicker.placeholder = placeHolderMessage; editorOverridePicker.canAcceptInBackground = true; @@ -458,7 +520,7 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } // If asked to always update the setting then update it even if the gear isn't clicked - if (alwaysUpdateSetting && result?.item.id) { + if (showDefaultPicker && result?.item.id) { this.updateUserAssociations(`*${extname(resource)}`, result.item.id,); } @@ -487,6 +549,11 @@ export class EditorOverrideService extends Disposable implements IEditorOverride // options and group to use accordingly if (picked) { + // If the user selected to configure default we trigger this picker again and tell it to show the default picker + if (picked.item.id === EditorOverrideService.configureDefaultID) { + return this.doPickEditorOverride(editor, options, group, true); + } + // Figure out target group let targetGroup: IEditorGroup | undefined; if (picked.keyMods?.alt || picked.keyMods?.ctrlCmd) { @@ -520,13 +587,12 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } } - private cacheContributionPoints() { - // Create a set to store contributed glob patterns + private cacheEditors() { + // Create a set to store glob patterns const cacheStorage: Set = new Set(); // Store just the relative pattern pieces without any path info - for (const globPattern of this._contributionPoints.keys()) { - const contribPoint = this._contributionPoints.get(globPattern)!; + for (const [globPattern, contribPoint] of this._editors) { const nonOptional = !!contribPoint.find(c => c.editorInfo.priority !== ContributedEditorPriority.option && c.editorInfo.id !== DEFAULT_EDITOR_ASSOCIATION.id); // Don't keep a cache of the optional ones as those wouldn't be opened on start anyways if (!nonOptional) { @@ -540,7 +606,7 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } // Also store the users settings as those would have to activate on startup as well - const userAssociations = this.configurationService.getValue(editorsAssociationsSettingId) || []; + const userAssociations = this.getAllUserAssociations(); for (const association of userAssociations) { if (association.filenamePattern) { cacheStorage.add(association.filenamePattern); diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index a141a57e29..1422d18fa8 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -4,9 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IResourceEditorInput, ITextEditorOptions, IEditorOptions, EditorActivation, EditorOverride, IResourceEditorInputIdentifier } from 'vs/platform/editor/common/editor'; -import { SideBySideEditor, IEditorInput, IEditorPane, GroupIdentifier, IFileEditorInput, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorInputFactoryRegistry, EditorExtensions, EditorInput, SideBySideEditorInput, IEditorInputWithOptions, isEditorInputWithOptions, EditorOptions, TextEditorOptions, IEditorIdentifier, IEditorCloseEvent, ITextEditorPane, ITextDiffEditorPane, IRevertOptions, SaveReason, EditorsOrder, isTextEditorPane, IWorkbenchEditorConfiguration, EditorResourceAccessor, IVisibleEditorPane, IEditorInputWithOptionsAndGroup } from 'vs/workbench/common/editor'; -import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; +import { IResourceEditorInput, IEditorOptions, EditorActivation, EditorOverride, IResourceEditorInputIdentifier, ITextEditorOptions, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { SideBySideEditor, IEditorInput, IEditorPane, GroupIdentifier, IFileEditorInput, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorInputFactoryRegistry, EditorExtensions, IEditorInputWithOptions, isEditorInputWithOptions, IEditorIdentifier, IEditorCloseEvent, ITextEditorPane, ITextDiffEditorPane, IRevertOptions, SaveReason, EditorsOrder, isTextEditorPane, IWorkbenchEditorConfiguration, EditorResourceAccessor, IVisibleEditorPane, IEditorInputWithOptionsAndGroup, EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; +import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { Registry } from 'vs/platform/registry/common/platform'; import { ResourceMap } from 'vs/base/common/map'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; @@ -14,30 +16,30 @@ import { IFileService, FileOperationEvent, FileOperation, FileChangesEvent, File import { Schemas } from 'vs/base/common/network'; import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { basename, joinPath, isEqual } from 'vs/base/common/resources'; +import { basename, joinPath } from 'vs/base/common/resources'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, GroupChangeKind, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IResourceEditorInputType, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler, IEditorService, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE, ISaveEditorsOptions, ISaveAllEditorsOptions, IRevertAllEditorsOptions, IBaseSaveRevertAllEditorOptions, IOpenEditorOverrideEntry } from 'vs/workbench/services/editor/common/editorService'; +import { IResourceEditorInputType, SIDE_GROUP, IResourceEditorReplacement, IEditorService, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE, ISaveEditorsOptions, ISaveAllEditorsOptions, IRevertAllEditorsOptions, IBaseSaveRevertAllEditorOptions, IOpenEditorsOptions } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { Disposable, IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { coalesce, distinct, firstOrDefault, insert } from 'vs/base/common/arrays'; +import { Disposable, IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; +import { coalesce, distinct } from 'vs/base/common/arrays'; import { isCodeEditor, isDiffEditor, ICodeEditor, IDiffEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorGroupView, EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { isUndefined, withNullAsUndefined } from 'vs/base/common/types'; import { EditorsObserver } from 'vs/workbench/browser/parts/editor/editorsObserver'; -import { IEditorViewState } from 'vs/editor/common/editorCommon'; import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { Promises, timeout } from 'vs/base/common/async'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { indexOfPath } from 'vs/base/common/extpath'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { ILogService } from 'vs/platform/log/common/log'; import { ContributedEditorPriority, DEFAULT_EDITOR_ASSOCIATION, IEditorOverrideService } from 'vs/workbench/services/editor/common/editorOverrideService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkspaceTrustRequestService, WorkspaceTrustUriResponse } from 'vs/platform/workspace/common/workspaceTrust'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; -type CachedEditorInput = ResourceEditorInput | IFileEditorInput | UntitledTextEditorInput; +type CachedEditorInput = TextResourceEditorInput | IFileEditorInput | UntitledTextEditorInput; type OpenInEditorGroup = IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE; export class EditorService extends Disposable implements EditorServiceImpl { @@ -73,8 +75,10 @@ export class EditorService extends Disposable implements EditorServiceImpl { @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @ILogService private readonly logService: ILogService, @IEditorOverrideService private readonly editorOverrideService: IEditorOverrideService, + @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, + @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, + @IHostService private readonly hostService: IHostService, ) { super(); @@ -110,6 +114,22 @@ export class EditorService extends Disposable implements EditorServiceImpl { this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue()))); } + private registerDefaultOverride(): void { + this._register(this.editorOverrideService.registerEditor( + '*', + { + id: DEFAULT_EDITOR_ASSOCIATION.id, + label: DEFAULT_EDITOR_ASSOCIATION.displayName, + detail: DEFAULT_EDITOR_ASSOCIATION.providerDisplayName, + describes: (currentEditor) => this.fileEditorInputFactory.isFileEditorInput(currentEditor) && currentEditor.matches(this.activeEditor), + priority: ContributedEditorPriority.builtin + }, + {}, + resource => ({ editor: this.createEditorInput({ resource }) }), + diffEditor => ({ editor: diffEditor }) + )); + } + //#region Editor & group event handlers private lastActiveEditor: IEditorInput | undefined = undefined; @@ -284,7 +304,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { replacement: { ...moveResult.editor, options: { - ...moveResult.editor.options, + ...(moveResult.editor as IResourceEditorInputType).options, ...optionOverrides } } @@ -318,10 +338,11 @@ export class EditorService extends Disposable implements EditorServiceImpl { } // Handle deletes in opened editors depending on: - // - the user has not disabled the setting closeOnFileDelete - // - the file change is local - // - the input is a file that is not resolved (we need to dispose because we cannot restore otherwise since we do not have the contents) - if (this.closeOnFileDelete || !isExternal || (this.fileEditorInputFactory.isFileEditorInput(editor) && !editor.isResolved())) { + // - we close any editor when `closeOnFileDelete: true` + // - we close any editor when the delete occurred from within VSCode + // - we close any editor without resolved working copy assuming that + // this editor could not be opened after the file is gone + if (this.closeOnFileDelete || !isExternal || !this.workingCopyService.has(resource)) { // Do NOT close any opened editor that matches the resource path (either equal or being parent) of the // resource we move to (movedTo). Otherwise we would close a resource that has been renamed to the same @@ -365,7 +386,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { const editors: IEditorInput[] = []; function conditionallyAddEditor(editor: IEditorInput): void { - if (editor.isUntitled() && !options.includeUntitled) { + if (editor.hasCapability(EditorInputCapabilities.Untitled) && !options.includeUntitled) { return; } @@ -485,164 +506,48 @@ export class EditorService extends Disposable implements EditorServiceImpl { //#endregion - //#region editor overrides - - private readonly openEditorOverrides: IOpenEditorOverrideHandler[] = []; - - overrideOpenEditor(handler: IOpenEditorOverrideHandler): IDisposable { - const remove = insert(this.openEditorOverrides, handler); - - return toDisposable(() => remove()); - } - - getEditorOverrides(resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined): [IOpenEditorOverrideHandler, IOpenEditorOverrideEntry][] { - const overrides: [IOpenEditorOverrideHandler, IOpenEditorOverrideEntry][] = []; - - // Collect contributed editor open overrides - for (const openEditorOverride of this.openEditorOverrides) { - if (typeof openEditorOverride.getEditorOverrides === 'function') { - try { - overrides.push(...openEditorOverride.getEditorOverrides(resource, options, group).map(val => [openEditorOverride, val] as [IOpenEditorOverrideHandler, IOpenEditorOverrideEntry])); - } catch (error) { - this.logService.error(`Unexpected error getting editor overrides: ${error}`); - } - } - } - - // Ensure the default one is always present - if (!overrides.some(([, entry]) => entry.id === DEFAULT_EDITOR_ASSOCIATION.id)) { - overrides.unshift(this.getDefaultEditorOverride(resource)); - } - - return overrides; - } - - private registerDefaultOverride(): void { - this._register(this.editorOverrideService.registerContributionPoint( - '*', - { - id: DEFAULT_EDITOR_ASSOCIATION.id, - label: DEFAULT_EDITOR_ASSOCIATION.displayName, - detail: DEFAULT_EDITOR_ASSOCIATION.providerDisplayName, - describes: (currentEditor) => currentEditor.matches(this.activeEditor), - priority: ContributedEditorPriority.builtin - }, - {}, - resource => ({ editor: this.createEditorInput({ resource }) }), - diffEditor => ({ editor: diffEditor }) - )); - } - - private getDefaultEditorOverride(resource: URI): [IOpenEditorOverrideHandler, IOpenEditorOverrideEntry] { - return [ - { - open: (editor: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup) => { - const resource = EditorResourceAccessor.getOriginalUri(editor); - if (!resource) { - return undefined; // {{SQL CARBON EDIT}} Strict null - } - - const fileEditorInput = this.createEditorInput({ resource, forceFile: true }); - const textOptions: IEditorOptions | ITextEditorOptions = { ...options, override: EditorOverride.DISABLED }; - return { - override: (async () => { - - // Try to replace existing editors for resource - const existingEditor = firstOrDefault(this.findEditors(resource, group)); - if (existingEditor && !fileEditorInput.matches(existingEditor)) { - await this.replaceEditors([{ - editor: existingEditor, - replacement: fileEditorInput, - forceReplaceDirty: existingEditor.resource?.scheme === Schemas.untitled, - options: options ? EditorOptions.create(options) : undefined, - }], group); - } - - return this.openEditor(fileEditorInput, textOptions, group); - })() - }; - } - }, - { - id: DEFAULT_EDITOR_ASSOCIATION.id, - label: DEFAULT_EDITOR_ASSOCIATION.displayName, - detail: DEFAULT_EDITOR_ASSOCIATION.providerDisplayName, - active: this.fileEditorInputFactory.isFileEditorInput(this.activeEditor) && isEqual(this.activeEditor.resource, resource), - } - ]; - } - - private doOverrideOpenEditor(editor: IEditorInput, options: IEditorOptions | undefined, group: IEditorGroup): Promise | undefined { - for (const openEditorOverride of this.openEditorOverrides) { - const result = openEditorOverride.open(editor, options, group); - const override = result?.override; - if (override) { - return override; - } - } - - return undefined; // {{SQL CARBON EDIT}} Strict null - } - - //#endregion - //#region openEditor() - openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: OpenInEditorGroup): Promise; + openEditor(editor: IEditorInput, options?: IEditorOptions, group?: OpenInEditorGroup): Promise; openEditor(editor: IResourceEditorInput | IUntitledTextResourceEditorInput, group?: OpenInEditorGroup): Promise; openEditor(editor: IResourceDiffEditorInput, group?: OpenInEditorGroup): Promise; - async openEditor(editor: IEditorInput | IResourceEditorInputType, optionsOrGroup?: IEditorOptions | ITextEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): Promise { + async openEditor(editor: IEditorInput | IResourceEditorInputType, optionsOrGroup?: IEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): Promise { const result = this.doResolveEditorOpenRequest(editor, optionsOrGroup, group); if (result) { const [resolvedGroup, resolvedEditor, resolvedOptions] = result; - // Override handling: pick editor or open specific - if (resolvedOptions?.override === EditorOverride.PICK || typeof resolvedOptions?.override === 'string') { - const resolvedInputWithOptionsAndGroup = await this.editorOverrideService.resolveEditorOverride(resolvedEditor, resolvedOptions, resolvedGroup); - if (!resolvedInputWithOptionsAndGroup) { - return undefined; // no editor was picked or registered for the identifier - } - - return (resolvedInputWithOptionsAndGroup.group ?? resolvedGroup).openEditor(resolvedInputWithOptionsAndGroup.editor, resolvedInputWithOptionsAndGroup.options ?? resolvedOptions); - } - - // Override handling: ask providers to override + // Override handling: request override from override service if (resolvedOptions?.override !== EditorOverride.DISABLED) { - // TODO@lramos15 this will get cleaned up soon, but since the override - // service no longer uses the override flow we must check that const resolvedInputWithOptionsAndGroup = await this.editorOverrideService.resolveEditorOverride(resolvedEditor, resolvedOptions, resolvedGroup); - // If we didn't override try the legacy overrides - if (!resolvedInputWithOptionsAndGroup || resolvedEditor.matches(resolvedInputWithOptionsAndGroup.editor)) { - const override = this.doOverrideOpenEditor(resolvedEditor, resolvedOptions, resolvedGroup); - if (override) { - return override; - } - } else { - return (resolvedInputWithOptionsAndGroup.group ?? resolvedGroup).openEditor(resolvedInputWithOptionsAndGroup.editor, resolvedInputWithOptionsAndGroup.options ?? resolvedOptions); + if (resolvedInputWithOptionsAndGroup) { + return (resolvedInputWithOptionsAndGroup.group ?? resolvedGroup).openEditor( + resolvedInputWithOptionsAndGroup.editor, + resolvedInputWithOptionsAndGroup.options ?? resolvedOptions + ); } } - // Override handling: disabled + // Override handling: disabled or no override found return resolvedGroup.openEditor(resolvedEditor, resolvedOptions); } return undefined; } - doResolveEditorOpenRequest(editor: IEditorInput | IResourceEditorInputType, optionsOrGroup?: IEditorOptions | ITextEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): [IEditorGroup, EditorInput, EditorOptions | undefined] | undefined { + doResolveEditorOpenRequest(editor: IEditorInput | IResourceEditorInputType, optionsOrGroup?: IEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): [IEditorGroup, EditorInput, IEditorOptions | undefined] | undefined { let resolvedGroup: IEditorGroup | undefined; let candidateGroup: OpenInEditorGroup | undefined; let typedEditor: EditorInput | undefined; - let typedOptions: EditorOptions | undefined; + let options: IEditorOptions | undefined; // Typed Editor Support if (editor instanceof EditorInput) { typedEditor = editor; - typedOptions = this.toOptions(optionsOrGroup as IEditorOptions); + options = optionsOrGroup as IEditorOptions; candidateGroup = group; - resolvedGroup = this.findTargetGroup(typedEditor, typedOptions, candidateGroup); + resolvedGroup = this.findTargetGroup(typedEditor, options, candidateGroup); } // Untyped Text Editor Support @@ -650,19 +555,19 @@ export class EditorService extends Disposable implements EditorServiceImpl { const textInput = editor as IResourceEditorInputType; typedEditor = this.createEditorInput(textInput); if (typedEditor) { - typedOptions = TextEditorOptions.from(textInput); + options = textInput.options; candidateGroup = optionsOrGroup as OpenInEditorGroup; - resolvedGroup = this.findTargetGroup(typedEditor, typedOptions, candidateGroup); + resolvedGroup = this.findTargetGroup(typedEditor, options, candidateGroup); } } if (typedEditor && resolvedGroup) { if ( this.editorGroupService.activeGroup !== resolvedGroup && // only if target group is not already active - typedOptions && !typedOptions.inactive && // never for inactive editors - typedOptions.preserveFocus && // only if preserveFocus - typeof typedOptions.activation !== 'number' && // only if activation is not already defined (either true or false) + options && !options.inactive && // never for inactive editors + options.preserveFocus && // only if preserveFocus + typeof options.activation !== 'number' && // only if activation is not already defined (either true or false) candidateGroup !== SIDE_GROUP // never for the SIDE_GROUP ) { // If the resolved group is not the active one, we typically @@ -674,10 +579,10 @@ export class EditorService extends Disposable implements EditorServiceImpl { // group is it is opened as `SIDE_GROUP` with `preserveFocus:true`. // repeated Alt-clicking of files in the explorer always open // into the same side group and not cause a group to be created each time. - typedOptions.overwrite({ activation: EditorActivation.ACTIVATE }); + options.activation = EditorActivation.ACTIVATE; } - return [resolvedGroup, typedEditor, typedOptions]; + return [resolvedGroup, typedEditor, options]; } return undefined; @@ -763,26 +668,23 @@ export class EditorService extends Disposable implements EditorServiceImpl { return neighbourGroup; } - private toOptions(options?: IEditorOptions | ITextEditorOptions | EditorOptions): EditorOptions { - if (!options || options instanceof EditorOptions) { - return options as EditorOptions; - } - - const textOptions: ITextEditorOptions = options; - if (textOptions.selection || textOptions.viewState) { - return TextEditorOptions.create(options); - } - - return EditorOptions.create(options); - } - //#endregion //#region openEditors() - openEditors(editors: IEditorInputWithOptions[], group?: OpenInEditorGroup): Promise; - openEditors(editors: IResourceEditorInputType[], group?: OpenInEditorGroup): Promise; - async openEditors(editors: Array, group?: OpenInEditorGroup): Promise { + openEditors(editors: IEditorInputWithOptions[], group?: OpenInEditorGroup, options?: IOpenEditorsOptions): Promise; + openEditors(editors: IResourceEditorInputType[], group?: OpenInEditorGroup, options?: IOpenEditorsOptions): Promise; + async openEditors(editors: Array, group?: OpenInEditorGroup, options?: IOpenEditorsOptions): Promise { + + // Pass all editors to trust service to determine if + // we should proceed with opening the editors if we + // are asked to validate trust. + if (options?.validateTrust) { + const editorsTrusted = await this.handleWorkspaceTrust(editors); + if (!editorsTrusted) { + return []; + } + } // Convert to typed editors and options const typedEditors: IEditorInputWithOptions[] = editors.map(editor => { @@ -792,7 +694,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { return { editor: this.createEditorInput(editor), - options: TextEditorOptions.from(editor) + options: editor.options }; }); @@ -830,7 +732,10 @@ export class EditorService extends Disposable implements EditorServiceImpl { mapGroupToEditors.set(targetGroup, targetGroupEditors); } - targetGroupEditors.push(editorOverride ?? { editor, options }); + targetGroupEditors.push(editorOverride ? + { editor: editorOverride.editor, options: editorOverride.options } : + { editor, options } + ); } } @@ -843,13 +748,82 @@ export class EditorService extends Disposable implements EditorServiceImpl { return coalesce(await Promises.settled(result)); } + private async handleWorkspaceTrust(editors: Array): Promise { + const { resources, diffMode } = this.extractEditorResources(editors); + + const trustResult = await this.workspaceTrustRequestService.requestOpenUris(resources); + switch (trustResult) { + case WorkspaceTrustUriResponse.Open: + return true; + case WorkspaceTrustUriResponse.OpenInNewWindow: + await this.hostService.openWindow(resources.map(resource => ({ fileUri: resource })), { forceNewWindow: true, diffMode }); + return false; + case WorkspaceTrustUriResponse.Cancel: + return false; + } + } + + private extractEditorResources(editors: Array): { resources: URI[], diffMode?: boolean } { + const resources = new ResourceMap(); + let diffMode = false; + + for (const editor of editors) { + + // Typed Editor + if (isEditorInputWithOptions(editor)) { + const resource = EditorResourceAccessor.getOriginalUri(editor.editor, { supportSideBySide: SideBySideEditor.BOTH }); + if (URI.isUri(resource)) { + resources.set(resource, true); + } else if (resource) { + if (resource.primary) { + resources.set(resource.primary, true); + } + + if (resource.secondary) { + resources.set(resource.secondary, true); + } + + diffMode = editor.editor instanceof DiffEditorInput; + } + } + + // Untyped editor + else { + const resourceDiffEditor = editor as IResourceDiffEditorInput; + if (resourceDiffEditor.originalInput && resourceDiffEditor.modifiedInput) { + const originalResourceEditor = resourceDiffEditor.originalInput as IResourceEditorInput; + if (URI.isUri(originalResourceEditor.resource)) { + resources.set(originalResourceEditor.resource, true); + } + + const modifiedResourceEditor = resourceDiffEditor.modifiedInput as IResourceEditorInput; + if (URI.isUri(modifiedResourceEditor.resource)) { + resources.set(modifiedResourceEditor.resource, true); + } + + diffMode = true; + } + + const resourceEditor = editor as IResourceEditorInput; + if (URI.isUri(resourceEditor.resource)) { + resources.set(resourceEditor.resource, true); + } + } + } + + return { + resources: Array.from(resources.keys()), + diffMode + }; + } + //#endregion //#region isOpened() isOpened(editor: IResourceEditorInputIdentifier): boolean { return this.editorsObserver.hasEditor({ - resource: this.asCanonicalEditorResource(editor.resource), + resource: this.uriIdentityService.asCanonicalUri(editor.resource), typeId: editor.typeId }); } @@ -956,7 +930,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { editor: replacementArg.editor, replacement: replacementArg.replacement, forceReplaceDirty: replacementArg.forceReplaceDirty, - options: this.toOptions(replacementArg.options) + options: replacementArg.options }); } else { const replacementArg = replaceEditorArg as IResourceEditorReplacement; @@ -964,7 +938,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { typedEditors.push({ editor: this.createEditorInput(replacementArg.editor), replacement: this.createEditorInput(replacementArg.replacement), - options: this.toOptions(replacementArg.replacement.options) + options: replacementArg.replacement.options }); } } @@ -995,22 +969,22 @@ export class EditorService extends Disposable implements EditorServiceImpl { // Diff Editor Support const resourceDiffInput = input as IResourceDiffEditorInput; - if (resourceDiffInput.leftResource && resourceDiffInput.rightResource) { - const leftInput = this.createEditorInput({ resource: resourceDiffInput.leftResource, forceFile: resourceDiffInput.forceFile }); - const rightInput = this.createEditorInput({ resource: resourceDiffInput.rightResource, forceFile: resourceDiffInput.forceFile }); + if (resourceDiffInput.originalInput && resourceDiffInput.modifiedInput) { + const originalInput = this.createEditorInput({ ...resourceDiffInput.originalInput, forceFile: resourceDiffInput.forceFile }); + const modifiedInput = this.createEditorInput({ ...resourceDiffInput.modifiedInput, forceFile: resourceDiffInput.forceFile }); return this.instantiationService.createInstance(DiffEditorInput, resourceDiffInput.label, resourceDiffInput.description, - leftInput, - rightInput, + originalInput, + modifiedInput, undefined ); } // Untitled file support const untitledInput = input as IUntitledTextResourceEditorInput; - if (untitledInput.forceUntitled || !untitledInput.resource || (untitledInput.resource && untitledInput.resource.scheme === Schemas.untitled)) { + if (untitledInput.forceUntitled || !untitledInput.resource || (untitledInput.resource.scheme === Schemas.untitled)) { const untitledOptions = { mode: untitledInput.mode, initialValue: untitledInput.contents, @@ -1044,31 +1018,31 @@ export class EditorService extends Disposable implements EditorServiceImpl { }) as EditorInput; } - // Resource Editor Support - const resourceEditorInput = input as IResourceEditorInput; - if (resourceEditorInput.resource instanceof URI) { + // Text Resource Editor Support + const textResourceEditorInput = input as ITextResourceEditorInput; + if (textResourceEditorInput.resource instanceof URI) { // Derive the label from the path if not provided explicitly - const label = resourceEditorInput.label || basename(resourceEditorInput.resource); + const label = textResourceEditorInput.label || basename(textResourceEditorInput.resource); // We keep track of the preferred resource this input is to be created // with but it may be different from the canonical resource (see below) - const preferredResource = resourceEditorInput.resource; + const preferredResource = textResourceEditorInput.resource; // From this moment on, only operate on the canonical resource // to ensure we reduce the chance of opening the same resource // with different resource forms (e.g. path casing on Windows) - const canonicalResource = this.asCanonicalEditorResource(preferredResource); + const canonicalResource = this.uriIdentityService.asCanonicalUri(preferredResource); return this.createOrGetCached(canonicalResource, () => { // File - if (resourceEditorInput.forceFile || this.fileService.canHandleResource(canonicalResource)) { - return this.fileEditorInputFactory.createFileEditorInput(canonicalResource, preferredResource, resourceEditorInput.label, resourceEditorInput.description, resourceEditorInput.encoding, resourceEditorInput.mode, this.instantiationService); + if (textResourceEditorInput.forceFile || this.fileService.canHandleResource(canonicalResource)) { + return this.fileEditorInputFactory.createFileEditorInput(canonicalResource, preferredResource, textResourceEditorInput.label, textResourceEditorInput.description, textResourceEditorInput.encoding, textResourceEditorInput.mode, textResourceEditorInput.contents, this.instantiationService); } // Resource - return this.instantiationService.createInstance(ResourceEditorInput, canonicalResource, resourceEditorInput.label, resourceEditorInput.description, resourceEditorInput.mode); + return this.instantiationService.createInstance(TextResourceEditorInput, canonicalResource, textResourceEditorInput.label, textResourceEditorInput.description, textResourceEditorInput.mode, textResourceEditorInput.contents); }, cachedInput => { // Untitled @@ -1077,23 +1051,27 @@ export class EditorService extends Disposable implements EditorServiceImpl { } // Files - else if (!(cachedInput instanceof ResourceEditorInput)) { + else if (!(cachedInput instanceof TextResourceEditorInput)) { cachedInput.setPreferredResource(preferredResource); - if (resourceEditorInput.label) { - cachedInput.setPreferredName(resourceEditorInput.label); + if (textResourceEditorInput.label) { + cachedInput.setPreferredName(textResourceEditorInput.label); } - if (resourceEditorInput.description) { - cachedInput.setPreferredDescription(resourceEditorInput.description); + if (textResourceEditorInput.description) { + cachedInput.setPreferredDescription(textResourceEditorInput.description); } - if (resourceEditorInput.encoding) { - cachedInput.setPreferredEncoding(resourceEditorInput.encoding); + if (textResourceEditorInput.encoding) { + cachedInput.setPreferredEncoding(textResourceEditorInput.encoding); } - if (resourceEditorInput.mode) { - cachedInput.setPreferredMode(resourceEditorInput.mode); + if (textResourceEditorInput.mode) { + cachedInput.setPreferredMode(textResourceEditorInput.mode); + } + + if (typeof textResourceEditorInput.contents === 'string') { + cachedInput.setPreferredContents(textResourceEditorInput.contents); } } @@ -1103,12 +1081,16 @@ export class EditorService extends Disposable implements EditorServiceImpl { cachedInput.setName(label); } - if (resourceEditorInput.description) { - cachedInput.setDescription(resourceEditorInput.description); + if (textResourceEditorInput.description) { + cachedInput.setDescription(textResourceEditorInput.description); } - if (resourceEditorInput.mode) { - cachedInput.setPreferredMode(resourceEditorInput.mode); + if (textResourceEditorInput.mode) { + cachedInput.setPreferredMode(textResourceEditorInput.mode); + } + + if (typeof textResourceEditorInput.contents === 'string') { + cachedInput.setPreferredContents(textResourceEditorInput.contents); } } }) as EditorInput; @@ -1117,28 +1099,6 @@ export class EditorService extends Disposable implements EditorServiceImpl { throw new Error('Unknown input type'); } - private _modelService: IModelService | undefined = undefined; - private get modelService(): IModelService | undefined { - if (!this._modelService) { - this._modelService = this.instantiationService.invokeFunction(accessor => accessor.get(IModelService)); - } - - return this._modelService; - } - - private asCanonicalEditorResource(resource: URI): URI { - const canonicalResource: URI = this.uriIdentityService.asCanonicalUri(resource); - - // In the unlikely case that a model exists for the original resource but - // differs from the canonical resource, we print a warning as this means - // the model will not be able to be opened as editor. - if (!isEqual(resource, canonicalResource) && this.modelService?.getModel(resource)) { - this.logService.warn(`EditorService: a model exists for a resource that is not canonical: ${resource.toString(true)}`); - } - - return canonicalResource; - } - private createOrGetCached(resource: URI, factoryFn: () => CachedEditorInput, cachedFn?: (input: CachedEditorInput) => void): CachedEditorInput { // Return early if already cached @@ -1185,7 +1145,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { editorsToSaveSequentially.push(...uniqueEditors); } else { for (const { groupId, editor } of uniqueEditors) { - if (editor.isUntitled()) { + if (editor.hasCapability(EditorInputCapabilities.Untitled)) { editorsToSaveSequentially.push({ groupId, editor }); } else { editorsToSaveParallel.push({ groupId, editor }); @@ -1214,11 +1174,11 @@ export class EditorService extends Disposable implements EditorServiceImpl { // Preserve view state by opening the editor first if the editor // is untitled or we "Save As". This also allows the user to review // the contents of the editor before making a decision. - let viewState: IEditorViewState | undefined = undefined; const editorPane = await this.openEditor(editor, undefined, groupId); - if (isTextEditorPane(editorPane)) { - viewState = editorPane.getViewState(); - } + const editorOptions: ITextEditorOptions = { + pinned: true, + viewState: isTextEditorPane(editorPane) ? editorPane.getViewState() : undefined + }; const result = options?.saveAs ? await editor.saveAs(groupId, options) : await editor.save(groupId, options); saveResults.push(result); @@ -1231,9 +1191,9 @@ export class EditorService extends Disposable implements EditorServiceImpl { // only selected group) if the resulting editor is different from the // current one. if (!result.matches(editor)) { - const targetGroups = editor.isUntitled() ? this.editorGroupService.groups.map(group => group.id) /* untitled replaces across all groups */ : [groupId]; + const targetGroups = editor.hasCapability(EditorInputCapabilities.Untitled) ? this.editorGroupService.groups.map(group => group.id) /* untitled replaces across all groups */ : [groupId]; for (const group of targetGroups) { - await this.replaceEditors([{ editor, replacement: result, options: { pinned: true, viewState } }], group); + await this.replaceEditors([{ editor, replacement: result, options: editorOptions }], group); } } } @@ -1280,7 +1240,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { continue; } - if (!options?.includeUntitled && editor.isUntitled()) { + if (!options?.includeUntitled && editor.hasCapability(EditorInputCapabilities.Untitled)) { continue; } @@ -1340,10 +1300,10 @@ export class DelegatingEditorService implements IEditorService { @IEditorService private editorService: EditorService ) { } - openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: OpenInEditorGroup): Promise; + openEditor(editor: IEditorInput, options?: IEditorOptions, group?: OpenInEditorGroup): Promise; openEditor(editor: IResourceEditorInput | IUntitledTextResourceEditorInput, group?: OpenInEditorGroup): Promise; openEditor(editor: IResourceDiffEditorInput, group?: OpenInEditorGroup): Promise; - async openEditor(editor: IEditorInput | IResourceEditorInputType, optionsOrGroup?: IEditorOptions | ITextEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): Promise { + async openEditor(editor: IEditorInput | IResourceEditorInputType, optionsOrGroup?: IEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): Promise { const result = this.editorService.doResolveEditorOpenRequest(editor, optionsOrGroup, group); if (result) { const [resolvedGroup, resolvedEditor, resolvedOptions] = result; @@ -1372,10 +1332,10 @@ export class DelegatingEditorService implements IEditorService { getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): readonly IEditorIdentifier[] { return this.editorService.getEditors(order, options); } - openEditors(editors: IEditorInputWithOptions[], group?: OpenInEditorGroup): Promise; - openEditors(editors: IResourceEditorInputType[], group?: OpenInEditorGroup): Promise; - openEditors(editors: Array, group?: OpenInEditorGroup): Promise { - return this.editorService.openEditors(editors, group); + openEditors(editors: IEditorInputWithOptions[], group?: OpenInEditorGroup, options?: IOpenEditorsOptions): Promise; + openEditors(editors: IResourceEditorInputType[], group?: OpenInEditorGroup, options?: IOpenEditorsOptions): Promise; + openEditors(editors: Array, group?: OpenInEditorGroup, options?: IOpenEditorsOptions): Promise { + return this.editorService.openEditors(editors, group, options); } replaceEditors(editors: IResourceEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise; @@ -1392,9 +1352,6 @@ export class DelegatingEditorService implements IEditorService { findEditors(resource: IResourceEditorInputIdentifier, group: IEditorGroup | GroupIdentifier): IEditorInput | undefined; findEditors(arg1: URI | IResourceEditorInputIdentifier, arg2?: IEditorGroup | GroupIdentifier): readonly IEditorIdentifier[] | readonly IEditorInput[] | IEditorInput | undefined { return this.editorService.findEditors(arg1, arg2); } - overrideOpenEditor(handler: IOpenEditorOverrideHandler): IDisposable { return this.editorService.overrideOpenEditor(handler); } - getEditorOverrides(resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined) { return this.editorService.getEditorOverrides(resource, options, group); } - createEditorInput(input: IResourceEditorInputType): IEditorInput { return this.editorService.createEditorInput(input); } save(editors: IEditorIdentifier | IEditorIdentifier[], options?: ISaveEditorsOptions): Promise { return this.editorService.save(editors, options); } diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index 7376f776b0..2e7d89ce73 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -5,8 +5,8 @@ import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorInput, IEditorPane, GroupIdentifier, IEditorInputWithOptions, CloseDirection, IEditorPartOptions, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane, IEditorCloseEvent, IEditorMoveEvent } from 'vs/workbench/common/editor'; -import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { IEditorInput, IEditorPane, GroupIdentifier, IEditorInputWithOptions, CloseDirection, IEditorPartOptions, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane, IEditorCloseEvent, IEditorMoveEvent, IEditorOpenEvent } from 'vs/workbench/common/editor'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IDimension } from 'vs/editor/common/editorCommon'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -101,7 +101,7 @@ export interface ICloseAllEditorsOptions { export interface IEditorReplacement { editor: IEditorInput; replacement: IEditorInput; - options?: IEditorOptions | ITextEditorOptions; + options?: IEditorOptions; /** * Skips asking the user for confirmation and doesn't @@ -217,11 +217,6 @@ export interface IEditorGroupsService { */ readonly whenRestored: Promise; - /** - * Will return `true` as soon as `whenRestored` is resolved. - */ - isRestored(): boolean; - /** * Find out if the editor group service has UI state to restore * from a previous session. @@ -377,6 +372,7 @@ export const enum GroupChangeKind { EDITOR_MOVE, EDITOR_ACTIVE, EDITOR_LABEL, + EDITOR_CAPABILITIES, EDITOR_PIN, EDITOR_STICKY, EDITOR_DIRTY @@ -417,6 +413,12 @@ export interface IEditorGroup { */ readonly onWillMoveEditor: Event; + /** + * An event that is fired when an editor is about to be opened + * in the group. + */ + readonly onWillOpenEditor: Event; + /** * A unique identifier of this group that remains identical even if the * group is moved to different locations. @@ -519,7 +521,7 @@ export interface IEditorGroup { * @returns a promise that resolves around an IEditor instance unless * the call failed, or the editor was not opened as active editor. */ - openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions): Promise; + openEditor(editor: IEditorInput, options?: IEditorOptions): Promise; /** * Opens editors in this group. @@ -556,14 +558,14 @@ export interface IEditorGroup { /** * Move an editor from this group either within this group or to another group. */ - moveEditor(editor: IEditorInput, target: IEditorGroup, options?: IEditorOptions | ITextEditorOptions): void; + moveEditor(editor: IEditorInput, target: IEditorGroup, options?: IEditorOptions): void; /** * Copy an editor from this group to another group. * * Note: It is currently not supported to show the same editor more than once in the same group. */ - copyEditor(editor: IEditorInput, target: IEditorGroup, options?: IEditorOptions | ITextEditorOptions): void; + copyEditor(editor: IEditorInput, target: IEditorGroup, options?: IEditorOptions): void; /** * Close an editor from the group. This may trigger a confirmation dialog if diff --git a/src/vs/workbench/services/editor/common/editorOverrideService.ts b/src/vs/workbench/services/editor/common/editorOverrideService.ts index 0ee1031cd5..586792843f 100644 --- a/src/vs/workbench/services/editor/common/editorOverrideService.ts +++ b/src/vs/workbench/services/editor/common/editorOverrideService.ts @@ -1,11 +1,9 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as glob from 'vs/base/common/glob'; -import { Event } from 'vs/base/common/event'; -import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { posix } from 'vs/base/common/path'; @@ -14,10 +12,10 @@ import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; import { Extensions as ConfigurationExtensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; -import { EditorExtensions, IEditorInput, IEditorInputWithOptions, IEditorInputWithOptionsAndGroup } from 'vs/workbench/common/editor'; +import { IEditorInput, IEditorInputWithOptions, IEditorInputWithOptionsAndGroup } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -25,7 +23,7 @@ export const IEditorOverrideService = createDecorator('e //#region Editor Associations -// Static values for editor contributions +// Static values for registered editors export type EditorAssociation = { readonly viewType: string; @@ -44,40 +42,14 @@ export const DEFAULT_EDITOR_ASSOCIATION: IEditorType = { const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); -const editorTypeSchemaAddition: IJSONSchema = { - type: 'string', - enum: [] -}; - const editorAssociationsConfigurationNode: IConfigurationNode = { ...workbenchConfigurationNodeBase, properties: { 'workbench.editorAssociations': { - type: 'array', - markdownDescription: localize('editor.editorAssociations', "Configure which editor to use for specific file types."), - items: { - type: 'object', - defaultSnippets: [{ - body: { - 'viewType': '$1', - 'filenamePattern': '$2' - } - }], - properties: { - 'viewType': { - anyOf: [ - { - type: 'string', - description: localize('editor.editorAssociations.viewType', "The unique id of the editor to use."), - }, - editorTypeSchemaAddition - ] - }, - 'filenamePattern': { - type: 'string', - description: localize('editor.editorAssociations.filenamePattern', "Glob pattern specifying which files the editor should be used for."), - } - } + type: 'object', + markdownDescription: localize('editor.editorAssociations', "Configure glob patterns to editors (e.g. `\"*.hex\": \"hexEditor.hexEdit\"`). These have precedence over the default behavior."), + additionalProperties: { + type: 'string' } } } @@ -89,68 +61,6 @@ export interface IEditorType { readonly providerDisplayName: string; } -export interface IEditorTypesHandler { - readonly onDidChangeEditorTypes: Event; - - getEditorTypes(): IEditorType[]; -} - -export interface IEditorAssociationsRegistry { - - /** - * Register handlers for editor types - */ - registerEditorTypesHandler(id: string, handler: IEditorTypesHandler): IDisposable; -} - -class EditorAssociationsRegistry implements IEditorAssociationsRegistry { - - private readonly editorTypesHandlers = new Map(); - - registerEditorTypesHandler(id: string, handler: IEditorTypesHandler): IDisposable { - if (this.editorTypesHandlers.has(id)) { - throw new Error(`An editor type handler with ${id} was already registered.`); - } - - this.editorTypesHandlers.set(id, handler); - this.updateEditorAssociationsSchema(); - - const editorTypeChangeEvent = handler.onDidChangeEditorTypes(() => { - this.updateEditorAssociationsSchema(); - }); - - return { - dispose: () => { - editorTypeChangeEvent.dispose(); - this.editorTypesHandlers.delete(id); - this.updateEditorAssociationsSchema(); - } - }; - } - - private updateEditorAssociationsSchema() { - const enumValues: string[] = []; - const enumDescriptions: string[] = []; - - const editorTypes: IEditorType[] = [DEFAULT_EDITOR_ASSOCIATION]; - - for (const [, handler] of this.editorTypesHandlers) { - editorTypes.push(...handler.getEditorTypes()); - } - - for (const { id, providerDisplayName } of editorTypes) { - enumValues.push(id); - enumDescriptions.push(localize('editorAssociations.viewType.sourceDescription', "Source: {0}", providerDisplayName)); - } - - editorTypeSchemaAddition.enum = enumValues; - editorTypeSchemaAddition.enumDescriptions = enumDescriptions; - - configurationRegistry.notifyConfigurationSchemaUpdated(editorAssociationsConfigurationNode); - } -} - -Registry.add(EditorExtensions.Associations, new EditorAssociationsRegistry()); configurationRegistry.registerConfiguration(editorAssociationsConfigurationNode); //#endregion @@ -162,7 +72,7 @@ export enum ContributedEditorPriority { default = 'default' } -export type ContributionPointOptions = { +export type RegisteredEditorOptions = { /** * If your editor cannot be opened in multiple groups for the same resource */ @@ -187,9 +97,9 @@ export type ContributedEditorInfo = { priority: ContributedEditorPriority; }; -export type EditorInputFactoryFunction = (resource: URI, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup) => IEditorInputWithOptions; +export type EditorInputFactoryFunction = (resource: URI, options: IEditorOptions | undefined, group: IEditorGroup) => IEditorInputWithOptions; -export type DiffEditorInputFactoryFunction = (diffEditorInput: DiffEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup) => IEditorInputWithOptions; +export type DiffEditorInputFactoryFunction = (diffEditorInput: DiffEditorInput, options: IEditorOptions | undefined, group: IEditorGroup) => IEditorInputWithOptions; export interface IEditorOverrideService { readonly _serviceBrand: undefined; @@ -208,16 +118,16 @@ export interface IEditorOverrideService { updateUserAssociations(globPattern: string, editorID: string): void; /** - * Registers a specific editor contribution. - * @param globPattern The glob pattern for this contribution point - * @param editorInfo Information about the contribution point - * @param options Specific options which apply to this contribution + * Registers a specific editor. + * @param globPattern The glob pattern for this registration + * @param editorInfo Information about the registration + * @param options Specific options which apply to this registration * @param createEditorInput The factory method for creating inputs */ - registerContributionPoint( + registerEditor( globPattern: string | glob.IRelativePattern, editorInfo: ContributedEditorInfo, - options: ContributionPointOptions, + options: RegisteredEditorOptions, createEditorInput: EditorInputFactoryFunction, createDiffEditorInput?: DiffEditorInputFactoryFunction ): IDisposable; @@ -229,7 +139,14 @@ export interface IEditorOverrideService { * @param group The current group * @returns An IEditorInputWithOptionsAndGroup if there is an available override or undefined if there is not */ - resolveEditorOverride(editor: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup): Promise; + resolveEditorOverride(editor: IEditorInput, options: IEditorOptions | undefined, group: IEditorGroup): Promise; + + /** + * Given a resource returns all the editor ids that match that resource + * @param resource The resource + * @returns A list of editor ids + */ + getEditorIds(resource: URI): string[]; } //#endregion @@ -260,7 +177,7 @@ export function globMatchesResource(globPattern: string | glob.IRelativePattern, return false; } const matchOnPath = typeof globPattern === 'string' && globPattern.indexOf(posix.sep) >= 0; - const target = matchOnPath ? resource.path : basename(resource); - return glob.match(globPattern, target.toLowerCase()); + const target = matchOnPath ? `${resource.scheme}:${resource.path}` : basename(resource); + return glob.match(typeof globPattern === 'string' ? globPattern.toLowerCase() : globPattern, target.toLowerCase()); } //#endregion diff --git a/src/vs/workbench/services/editor/common/editorService.ts b/src/vs/workbench/services/editor/common/editorService.ts index ce45e2dfba..b39e554faa 100644 --- a/src/vs/workbench/services/editor/common/editorService.ts +++ b/src/vs/workbench/services/editor/common/editorService.ts @@ -4,17 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IResourceEditorInput, IEditorOptions, ITextEditorOptions, IResourceEditorInputIdentifier } from 'vs/platform/editor/common/editor'; +import { IResourceEditorInput, IEditorOptions, IResourceEditorInputIdentifier, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IEditorInput, IEditorPane, GroupIdentifier, IEditorInputWithOptions, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, ITextEditorPane, ITextDiffEditorPane, IEditorIdentifier, ISaveOptions, IRevertOptions, EditorsOrder, IVisibleEditorPane, IEditorCloseEvent } from 'vs/workbench/common/editor'; import { Event } from 'vs/base/common/event'; import { IEditor, IDiffEditor } from 'vs/editor/common/editorCommon'; import { IEditorGroup, IEditorReplacement } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; export const IEditorService = createDecorator('editorService'); -export type IResourceEditorInputType = IResourceEditorInput | IUntitledTextResourceEditorInput | IResourceDiffEditorInput; +export type IResourceEditorInputType = IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput | IResourceDiffEditorInput; export interface IResourceEditorReplacement { readonly editor: IResourceEditorInputType; @@ -27,27 +26,6 @@ export type ACTIVE_GROUP_TYPE = typeof ACTIVE_GROUP; export const SIDE_GROUP = -2; export type SIDE_GROUP_TYPE = typeof SIDE_GROUP; -export interface IOpenEditorOverrideEntry { - readonly id: string; - readonly label: string; - readonly active: boolean; - readonly detail?: string; -} - -export interface IOpenEditorOverrideHandler { - open(editor: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup): IOpenEditorOverride | undefined; - getEditorOverrides?(resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined): IOpenEditorOverrideEntry[]; -} - -export interface IOpenEditorOverride { - - /** - * If defined, will prevent the opening of an editor and replace the resulting - * promise with the provided promise for the openEditor() call. - */ - override?: Promise; -} - export interface ISaveEditorsOptions extends ISaveOptions { /** @@ -73,6 +51,15 @@ export interface ISaveAllEditorsOptions extends ISaveEditorsOptions, IBaseSaveRe export interface IRevertAllEditorsOptions extends IRevertOptions, IBaseSaveRevertAllEditorOptions { } +export interface IOpenEditorsOptions { + + /** + * Whether to validate trust when opening editors + * that are potentially not inside the workspace. + */ + readonly validateTrust?: boolean; +} + export interface IEditorService { readonly _serviceBrand: undefined; @@ -80,14 +67,14 @@ export interface IEditorService { /** * Emitted when the currently active editor changes. * - * @see `IEditorService.activeEditorPane` + * @see {@link IEditorService.activeEditorPane} */ readonly onDidActiveEditorChange: Event; /** * Emitted when any of the current visible editors changes. * - * @see `IEditorService.visibleEditorPanes` + * @see {@link IEditorService.visibleEditorPanes} */ readonly onDidVisibleEditorsChange: Event; @@ -100,7 +87,7 @@ export interface IEditorService { * The currently active editor pane or `undefined` if none. The editor pane is * the workbench container for editors of any kind. * - * @see `IEditorService.activeEditor` for access to the active editor input + * @see {@link IEditorService.activeEditor} for access to the active editor input */ readonly activeEditorPane: IVisibleEditorPane | undefined; @@ -115,7 +102,7 @@ export interface IEditorService { * The currently active text editor control or `undefined` if there is currently no active * editor or the active editor widget is neither a text nor a diff editor. * - * @see `IEditorService.activeEditor` + * @see {@link IEditorService.activeEditor} */ readonly activeTextEditorControl: IEditor | IDiffEditor | undefined; @@ -129,7 +116,7 @@ export interface IEditorService { /** * All editor panes that are currently visible across all editor groups. * - * @see `IEditorService.visibleEditors` for access to the visible editor inputs + * @see {@link IEditorService.visibleEditors} for access to the visible editor inputs */ readonly visibleEditorPanes: readonly IVisibleEditorPane[]; @@ -179,8 +166,9 @@ export interface IEditorService { * @returns the editor that opened or `undefined` if the operation failed or the editor was not * opened to be active. */ - openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; - openEditor(editor: IResourceEditorInput | IUntitledTextResourceEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; + openEditor(editor: IEditorInput, options?: IEditorOptions, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; + openEditor(editor: IResourceEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; + openEditor(editor: ITextResourceEditorInput | IUntitledTextResourceEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; openEditor(editor: IResourceDiffEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; /** @@ -194,8 +182,8 @@ export interface IEditorService { * @returns the editors that opened. The array can be empty or have less elements for editors * that failed to open or were instructed to open as inactive. */ - openEditors(editors: IEditorInputWithOptions[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; - openEditors(editors: IResourceEditorInputType[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; + openEditors(editors: IEditorInputWithOptions[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE, options?: IOpenEditorsOptions): Promise; + openEditors(editors: IResourceEditorInputType[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE, options?: IOpenEditorsOptions): Promise; /** * Replaces editors in an editor group with the provided replacement. @@ -233,18 +221,6 @@ export interface IEditorService { findEditors(resource: URI, group: IEditorGroup | GroupIdentifier): readonly IEditorInput[]; findEditors(resource: IResourceEditorInputIdentifier, group: IEditorGroup | GroupIdentifier): IEditorInput | undefined; - /** - * Get all available editor overrides for the editor input. - */ - getEditorOverrides(resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined): [IOpenEditorOverrideHandler, IOpenEditorOverrideEntry][]; - - /** - * Allows to override the opening of editors by installing a handler that will - * be called each time an editor is about to open allowing to override the - * operation to open a different editor. - */ - overrideOpenEditor(handler: IOpenEditorOverrideHandler): IDisposable; - /** * Converts a lightweight input to a workbench editor input. */ diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index 52016f09b4..d3406828e1 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -6,14 +6,14 @@ import * as assert from 'assert'; import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput, TestEditorPart, ITestInstantiationService, TestServiceAccessor, createEditorPart } from 'vs/workbench/test/browser/workbenchTestServices'; import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupChangeKind, GroupLocation } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { EditorOptions, CloseDirection, IEditorPartOptions, EditorsOrder } from 'vs/workbench/common/editor'; +import { CloseDirection, IEditorPartOptions, EditorsOrder, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { MockScopableContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; -suite.skip('EditorGroupsService', () => { +suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} Skip suite const TEST_EDITOR_ID = 'MyFileEditorForEditorGroupService'; const TEST_EDITOR_INPUT_ID = 'testEditorInputForEditorGroupService'; @@ -196,10 +196,10 @@ suite.skip('EditorGroupsService', () => { const downGroup = part.addGroup(rightGroup, GroupDirection.DOWN); const rootGroupInput = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); - await rootGroup.openEditor(rootGroupInput, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(rootGroupInput, { pinned: true }); const rightGroupInput = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); - await rightGroup.openEditor(rightGroupInput, EditorOptions.create({ pinned: true })); + await rightGroup.openEditor(rightGroupInput, { pinned: true }); assert.strictEqual(part.groups.length, 3); @@ -293,7 +293,7 @@ suite.skip('EditorGroupsService', () => { const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); - await rootGroup.openEditor(input, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input, { pinned: true }); const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT, { activate: true }); const downGroup = part.copyGroup(rootGroup, rightGroup, GroupDirection.DOWN); assert.strictEqual(groupAddedCounter, 2); @@ -323,13 +323,13 @@ suite.skip('EditorGroupsService', () => { const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input1, { pinned: true }); const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); - await rightGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await rightGroup.openEditor(input2, { pinned: true }); const downGroup = part.copyGroup(rootGroup, rightGroup, GroupDirection.DOWN); - await downGroup.openEditor(input3, EditorOptions.create({ pinned: true })); + await downGroup.openEditor(input3, { pinned: true }); part.activateGroup(rootGroup); @@ -347,8 +347,6 @@ suite.skip('EditorGroupsService', () => { await part.whenReady; await part.whenRestored; - - assert.strictEqual(part.isRestored(), true); }); test('options', async () => { @@ -380,6 +378,7 @@ suite.skip('EditorGroupsService', () => { let editorCloseCounter = 0; let editorPinCounter = 0; let editorStickyCounter = 0; + let editorCapabilitiesCounter = 0; const editorGroupChangeListener = group.onDidGroupChange(e => { if (e.kind === GroupChangeKind.EDITOR_OPEN) { assert.ok(e.editor); @@ -393,6 +392,9 @@ suite.skip('EditorGroupsService', () => { } else if (e.kind === GroupChangeKind.EDITOR_PIN) { assert.ok(e.editor); editorPinCounter++; + } else if (e.kind === GroupChangeKind.EDITOR_CAPABILITIES) { + assert.ok(e.editor); + editorCapabilitiesCounter++; } else if (e.kind === GroupChangeKind.EDITOR_STICKY) { assert.ok(e.editor); editorStickyCounter++; @@ -412,8 +414,8 @@ suite.skip('EditorGroupsService', () => { const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); - await group.openEditor(input, EditorOptions.create({ pinned: true })); - await group.openEditor(inputInactive, EditorOptions.create({ inactive: true })); + await group.openEditor(input, { pinned: true }); + await group.openEditor(inputInactive, { inactive: true }); assert.strictEqual(group.isActive(input), true); assert.strictEqual(group.isActive(inputInactive), false); @@ -421,6 +423,7 @@ suite.skip('EditorGroupsService', () => { assert.strictEqual(group.contains(inputInactive), true); assert.strictEqual(group.isEmpty, false); assert.strictEqual(group.count, 2); + assert.strictEqual(editorCapabilitiesCounter, 0); assert.strictEqual(editorDidOpenCounter, 2); assert.strictEqual(activeEditorChangeCounter, 1); assert.strictEqual(group.getEditorByIndex(0), input); @@ -428,6 +431,12 @@ suite.skip('EditorGroupsService', () => { assert.strictEqual(group.getIndexOfEditor(input), 0); assert.strictEqual(group.getIndexOfEditor(inputInactive), 1); + input.capabilities = EditorInputCapabilities.RequiresTrust; + assert.strictEqual(editorCapabilitiesCounter, 1); + + inputInactive.capabilities = EditorInputCapabilities.Singleton; + assert.strictEqual(editorCapabilitiesCounter, 2); + assert.strictEqual(group.previewEditor, inputInactive); assert.strictEqual(group.isPinned(inputInactive), false); group.pinEditor(inputInactive); @@ -1146,8 +1155,8 @@ suite.skip('EditorGroupsService', () => { const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); - await group.openEditor(input, EditorOptions.create({ pinned: true })); - await group.openEditor(inputInactive, EditorOptions.create({ inactive: true })); + await group.openEditor(input, { pinned: true }); + await group.openEditor(inputInactive, { inactive: true }); assert.strictEqual(group.stickyCount, 0); assert.strictEqual(group.isSticky(input), false); @@ -1208,7 +1217,7 @@ suite.skip('EditorGroupsService', () => { const inputSticky = new TestFileEditorInput(URI.file('foo/bar/sticky'), TEST_EDITOR_INPUT_ID); - await group.openEditor(inputSticky, EditorOptions.create({ sticky: true })); + await group.openEditor(inputSticky, { sticky: true }); assert.strictEqual(group.stickyCount, 2); assert.strictEqual(group.isSticky(input), false); @@ -1219,7 +1228,7 @@ suite.skip('EditorGroupsService', () => { assert.strictEqual(group.getIndexOfEditor(inputSticky), 1); assert.strictEqual(group.getIndexOfEditor(input), 2); - await group.openEditor(input, EditorOptions.create({ sticky: true })); + await group.openEditor(input, { sticky: true }); assert.strictEqual(group.stickyCount, 3); assert.strictEqual(group.isSticky(input), true); @@ -1274,6 +1283,48 @@ suite.skip('EditorGroupsService', () => { rightGroupListener.dispose(); }); + test('onWillOpenEditor', async () => { + const [part] = await createPart(); + const group = part.activeGroup; + assert.strictEqual(group.isEmpty, true); + + const rightGroup = part.addGroup(group, GroupDirection.RIGHT); + + const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const secondInput = new TestFileEditorInput(URI.file('foo/bar/second'), TEST_EDITOR_INPUT_ID); + const thirdInput = new TestFileEditorInput(URI.file('foo/bar/third'), TEST_EDITOR_INPUT_ID); + + let leftFiredCount = 0; + const leftGroupListener = group.onWillOpenEditor(() => { + leftFiredCount++; + }); + + let rightFiredCount = 0; + const rightGroupListener = rightGroup.onWillOpenEditor(() => { + rightFiredCount++; + }); + + await group.openEditor(input); + assert.strictEqual(leftFiredCount, 1); + assert.strictEqual(rightFiredCount, 0); + + rightGroup.openEditor(secondInput); + assert.strictEqual(leftFiredCount, 1); + assert.strictEqual(rightFiredCount, 1); + + group.openEditor(thirdInput); + assert.strictEqual(leftFiredCount, 2); + assert.strictEqual(rightFiredCount, 1); + + // Ensure move fires the open event too + rightGroup.moveEditor(secondInput, group); + assert.strictEqual(leftFiredCount, 3); + assert.strictEqual(rightFiredCount, 1); + + leftGroupListener.dispose(); + rightGroupListener.dispose(); + }); + test('copyEditor with context (across groups)', async () => { const [part] = await createPart(); const group = part.activeGroup; diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index e19ec97d6b..bec820e9b8 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -1,23 +1,23 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { EditorActivation } from 'vs/platform/editor/common/editor'; +import { EditorActivation, EditorOverride } from 'vs/platform/editor/common/editor'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { EditorInput, EditorsOrder, SideBySideEditorInput } from 'vs/workbench/common/editor'; +import { EditorsOrder, IResourceDiffEditorInput } from 'vs/workbench/common/editor'; import { workbenchInstantiationService, TestServiceAccessor, registerTestEditor, TestFileEditorInput, ITestInstantiationService, registerTestResourceEditor, registerTestSideBySideEditor, createEditorPart } from 'vs/workbench/test/browser/workbenchTestServices'; -import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; +import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { EditorService, DelegatingEditorService } from 'vs/workbench/services/editor/browser/editorService'; import { IEditorGroup, IEditorGroupsService, GroupDirection, GroupsArrangement } from 'vs/workbench/services/editor/common/editorGroupsService'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { timeout } from 'vs/base/common/async'; import { toResource } from 'vs/base/test/common/utils'; @@ -30,6 +30,12 @@ import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { isLinux } from 'vs/base/common/platform'; import { MockScopableContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { ContributedEditorPriority } from 'vs/workbench/services/editor/common/editorOverrideService'; +import { IWorkspaceTrustRequestService, WorkspaceTrustUriResponse } from 'vs/platform/workspace/common/workspaceTrust'; +import { TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} Skip suite @@ -60,6 +66,7 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} Skip suite const part = await createEditorPart(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, part); + instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService(false)); const editorService = instantiationService.createInstance(EditorService); instantiationService.stub(IEditorService, editorService); @@ -226,6 +233,104 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} Skip suite assert.strictEqual(part.activeGroup.getIndexOfEditor(replaceInput), 0); }); + test('openEditors() handles workspace trust (typed editors)', async () => { + const [part, service, accessor] = await createEditorService(); + + const input1 = new TestFileEditorInput(URI.parse('my://resource1-openEditors'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); + + const input3 = new TestFileEditorInput(URI.parse('my://resource3-openEditors'), TEST_EDITOR_INPUT_ID); + const input4 = new TestFileEditorInput(URI.parse('my://resource4-openEditors'), TEST_EDITOR_INPUT_ID); + const sideBySideInput = new SideBySideEditorInput('side by side', undefined, input3, input4); + + const oldHandler = accessor.workspaceTrustRequestService.requestOpenUrisHandler; + + try { + + // Trust: cancel + let trustEditorUris: URI[] = []; + accessor.workspaceTrustRequestService.requestOpenUrisHandler = async uris => { + trustEditorUris = uris; + return WorkspaceTrustUriResponse.Cancel; + }; + + await service.openEditors([{ editor: input1 }, { editor: input2 }, { editor: sideBySideInput }], undefined, { validateTrust: true }); + assert.strictEqual(part.activeGroup.count, 0); + assert.strictEqual(trustEditorUris.length, 4); + assert.strictEqual(trustEditorUris.some(uri => uri.toString() === input1.resource.toString()), true); + assert.strictEqual(trustEditorUris.some(uri => uri.toString() === input2.resource.toString()), true); + assert.strictEqual(trustEditorUris.some(uri => uri.toString() === input3.resource.toString()), true); + assert.strictEqual(trustEditorUris.some(uri => uri.toString() === input4.resource.toString()), true); + + // Trust: open in new window + accessor.workspaceTrustRequestService.requestOpenUrisHandler = async uris => WorkspaceTrustUriResponse.OpenInNewWindow; + + await service.openEditors([{ editor: input1 }, { editor: input2 }, { editor: sideBySideInput }], undefined, { validateTrust: true }); + assert.strictEqual(part.activeGroup.count, 0); + + // Trust: allow + accessor.workspaceTrustRequestService.requestOpenUrisHandler = async uris => WorkspaceTrustUriResponse.Open; + + await service.openEditors([{ editor: input1 }, { editor: input2 }, { editor: sideBySideInput }], undefined, { validateTrust: true }); + assert.strictEqual(part.activeGroup.count, 3); + } finally { + accessor.workspaceTrustRequestService.requestOpenUrisHandler = oldHandler; + } + }); + + test('openEditors() ignores trust when `validateTrust: false', async () => { + const [part, service, accessor] = await createEditorService(); + + const input1 = new TestFileEditorInput(URI.parse('my://resource1-openEditors'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); + + const input3 = new TestFileEditorInput(URI.parse('my://resource3-openEditors'), TEST_EDITOR_INPUT_ID); + const input4 = new TestFileEditorInput(URI.parse('my://resource4-openEditors'), TEST_EDITOR_INPUT_ID); + const sideBySideInput = new SideBySideEditorInput('side by side', undefined, input3, input4); + + const oldHandler = accessor.workspaceTrustRequestService.requestOpenUrisHandler; + + try { + + // Trust: cancel + accessor.workspaceTrustRequestService.requestOpenUrisHandler = async uris => WorkspaceTrustUriResponse.Cancel; + + await service.openEditors([{ editor: input1 }, { editor: input2 }, { editor: sideBySideInput }]); + assert.strictEqual(part.activeGroup.count, 3); + } finally { + accessor.workspaceTrustRequestService.requestOpenUrisHandler = oldHandler; + } + }); + + test('openEditors() extracts proper resources from untyped editors for workspace trust', async () => { + const [part, service, accessor] = await createEditorService(); + + const input = { resource: URI.parse('my://resource-openEditors') }; + const otherInput: IResourceDiffEditorInput = { + originalInput: { resource: URI.parse('my://resource2-openEditors') }, + modifiedInput: { resource: URI.parse('my://resource3-openEditors') } + }; + + const oldHandler = accessor.workspaceTrustRequestService.requestOpenUrisHandler; + + try { + let trustEditorUris: URI[] = []; + accessor.workspaceTrustRequestService.requestOpenUrisHandler = async uris => { + trustEditorUris = uris; + return oldHandler(uris); + }; + + await service.openEditors([input, otherInput], undefined, { validateTrust: true }); + assert.strictEqual(part.activeGroup.count, 0); + assert.strictEqual(trustEditorUris.length, 3); + assert.strictEqual(trustEditorUris.some(uri => uri.toString() === input.resource.toString()), true); + assert.strictEqual(trustEditorUris.some(uri => uri.toString() === otherInput.originalInput.resource?.toString()), true); + assert.strictEqual(trustEditorUris.some(uri => uri.toString() === otherInput.modifiedInput.resource?.toString()), true); + } finally { + accessor.workspaceTrustRequestService.requestOpenUrisHandler = oldHandler; + } + }); + test('caching', function () { const instantiationService = workbenchInstantiationService(); const service = instantiationService.createInstance(EditorService); @@ -317,6 +422,16 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} Skip suite assert(input instanceof FileEditorInput); contentInput = input; assert.strictEqual(contentInput.getPreferredMode(), mode); + let fileModel = (await contentInput.resolve() as ITextFileEditorModel); + assert.strictEqual(fileModel.textEditorModel?.getModeId(), mode); + + // Untyped Input (file, contents) + input = service.createEditorInput({ resource: toResource.call(this, '/index.html'), contents: 'My contents' }); + assert(input instanceof FileEditorInput); + contentInput = input; + fileModel = (await contentInput.resolve() as ITextFileEditorModel); + assert.strictEqual(fileModel.textEditorModel?.getValue(), 'My contents'); + assert.strictEqual(fileModel.isDirty(), true); // Untyped Input (file, different mode) input = service.createEditorInput({ resource: toResource.call(this, '/index.html'), mode: 'text' }); @@ -361,14 +476,20 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} Skip suite // Untyped Input (resource) input = service.createEditorInput({ resource: URI.parse('custom:resource') }); - assert(input instanceof ResourceEditorInput); + assert(input instanceof TextResourceEditorInput); // Untyped Input (diff) - input = service.createEditorInput({ - leftResource: toResource.call(this, '/primary.html'), - rightResource: toResource.call(this, '/secondary.html') - }); + const resourceDiffInput = { + originalInput: { resource: toResource.call(this, '/primary.html') }, + modifiedInput: { resource: toResource.call(this, '/secondary.html') } + }; + input = service.createEditorInput(resourceDiffInput); assert(input instanceof DiffEditorInput); + assert.strictEqual(input.originalInput.resource?.toString(), resourceDiffInput.originalInput.resource.toString()); + assert.strictEqual(input.modifiedInput.resource?.toString(), resourceDiffInput.modifiedInput.resource.toString()); + const untypedDiffInput = input.asResourceEditorInput(0) as IResourceDiffEditorInput; + assert.strictEqual(untypedDiffInput.originalInput.resource?.toString(), resourceDiffInput.originalInput.resource.toString()); + assert.strictEqual(untypedDiffInput.modifiedInput.resource?.toString(), resourceDiffInput.modifiedInput.resource.toString()); }); test('delegate', function (done) { @@ -389,18 +510,18 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} Skip suite createEditor(): void { } } - const ed = instantiationService.createInstance(MyEditor, 'my.editor'); + const editor = instantiationService.createInstance(MyEditor, 'my.editor'); - const inp = instantiationService.createInstance(ResourceEditorInput, URI.parse('my://resource-delegate'), 'name', 'description', undefined); + const input = instantiationService.createInstance(TextResourceEditorInput, URI.parse('my://resource-delegate'), 'name', 'description', undefined, undefined); const delegate = instantiationService.createInstance(DelegatingEditorService, async (group, delegate) => { assert.ok(group); done(); - return ed; + return editor; }); - delegate.openEditor(inp); + delegate.openEditor(input); }); test('close editor does not dispose when editor opened in other group', async () => { @@ -955,7 +1076,8 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} Skip suite mtime: 0, name: 'resource2', size: 0, - isSymbolicLink: false + isSymbolicLink: false, + readonly: false })); await activeEditorChangePromise; @@ -998,32 +1120,105 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} Skip suite assert.strictEqual(editorContextKeyService, part.activeGroup.activeEditorPane?.scopedContextKeyService); }); - test('overrideOpenEditor', async function () { - const [, service] = await createEditorService(); + test('editorOverrideService - openEditor', async function () { + const [, service, accessor] = await createEditorService(); + const editorOverrideService = accessor.editorOverrideService; + let overrideCount = 0; + const registrationDisposable = editorOverrideService.registerEditor( + '*.md', + { + id: 'TestEditor', + label: 'Test Editor', + detail: 'Test Editor Provider', + describes: () => false, + priority: ContributedEditorPriority.builtin + }, + {}, + (resource) => { + overrideCount++; + return ({ editor: service.createEditorInput({ resource }) }); + }, + diffEditor => ({ editor: diffEditor }) + ); + assert.strictEqual(overrideCount, 0); + const input1 = new TestFileEditorInput(URI.parse('file://test/path/resource1.txt'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('file://test/path/resource2.md'), TEST_EDITOR_INPUT_ID); + // Open editor input 1 and it shouln't trigger override as the glob doesn't match + await service.openEditor(input1); + assert.strictEqual(overrideCount, 0); + // Open editor input 2 and it should trigger override as the glob doesn match + await service.openEditor(input2); + assert.strictEqual(overrideCount, 1); + // Because we specify an override we shouldn't see it triggered even if it matches + await service.openEditor(input2, { override: 'default' }); + assert.strictEqual(overrideCount, 1); - const input1 = new TestFileEditorInput(URI.parse('file://resource1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.parse('file://resource2'), TEST_EDITOR_INPUT_ID); + registrationDisposable.dispose(); + }); - let overrideCalled = false; + test('editorOverrideService - openEditors', async function () { + const [, service, accessor] = await createEditorService(); + const editorOverrideService = accessor.editorOverrideService; + let overrideCount = 0; + const registrationDisposable = editorOverrideService.registerEditor( + '*.md', + { + id: 'TestEditor', + label: 'Test Editor', + detail: 'Test Editor Provider', + describes: () => false, + priority: ContributedEditorPriority.builtin + }, + {}, + (resource) => { + overrideCount++; + return ({ editor: service.createEditorInput({ resource }) }); + }, + diffEditor => ({ editor: diffEditor }) + ); + assert.strictEqual(overrideCount, 0); + const input1 = new TestFileEditorInput(URI.parse('file://test/path/resource1.txt'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('file://test/path/resource2.txt'), TEST_EDITOR_INPUT_ID); + const input3 = new TestFileEditorInput(URI.parse('file://test/path/resource3.md'), TEST_EDITOR_INPUT_ID); + const input4 = new TestFileEditorInput(URI.parse('file://test/path/resource4.md'), TEST_EDITOR_INPUT_ID); + // Open editor input 1 and it shouln't trigger override as the glob doesn't match + await service.openEditors([{ editor: input1 }, { editor: input2 }, { editor: input3 }, { editor: input4 }]); + assert.strictEqual(overrideCount, 2); - const handler = service.overrideOpenEditor({ - open: editor => { - if (editor === input1) { - overrideCalled = true; + registrationDisposable.dispose(); + }); - return { override: service.openEditor(input2, { pinned: true }) }; - } - - return undefined; - } - }); - - await service.openEditor(input1, { pinned: true }); - - assert.ok(overrideCalled); - assert.strictEqual(service.activeEditor, input2); - - handler.dispose(); + test('editorOverrideService - replaceEditors', async function () { + const [part, service, accessor] = await createEditorService(); + const editorOverrideService = accessor.editorOverrideService; + let overrideCount = 0; + const registrationDisposable = editorOverrideService.registerEditor( + '*.md', + { + id: 'TestEditor', + label: 'Test Editor', + detail: 'Test Editor Provider', + describes: () => false, + priority: ContributedEditorPriority.builtin + }, + {}, + (resource) => { + overrideCount++; + return ({ editor: service.createEditorInput({ resource }) }); + }, + diffEditor => ({ editor: diffEditor }) + ); + assert.strictEqual(overrideCount, 0); + const input1 = new TestFileEditorInput(URI.parse('file://test/path/resource2.md'), TEST_EDITOR_INPUT_ID); + // Open editor input 1 and it shouldn't trigger because I've disabled the override logic + await service.openEditor(input1, { override: EditorOverride.DISABLED }); + assert.strictEqual(overrideCount, 0); + await service.replaceEditors([{ + editor: input1, + replacement: input1, + }], part.activeGroup); + assert.strictEqual(overrideCount, 1); + registrationDisposable.dispose(); }); test('findEditors (in group)', async () => { diff --git a/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts b/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts index 19648155b2..c5beb7c263 100644 --- a/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { EditorOptions, IEditorInputFactoryRegistry, EditorExtensions, SideBySideEditorInput } from 'vs/workbench/common/editor'; +import { IEditorInputFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; import { workbenchInstantiationService, TestFileEditorInput, registerTestEditor, TestEditorPart, createEditorPart, registerTestSideBySideEditor } from 'vs/workbench/test/browser/workbenchTestServices'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -17,8 +17,9 @@ import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { EditorsObserver } from 'vs/workbench/browser/parts/editor/editorsObserver'; import { timeout } from 'vs/base/common/async'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; -suite.skip('EditorsObserver', function () { +suite.skip('EditorsObserver', function () { // {{SQL CARBON EDIT}} Skip suite const TEST_EDITOR_ID = 'MyTestEditorForEditorsObserver'; const TEST_EDITOR_INPUT_ID = 'testEditorInputForEditorsObserver'; @@ -26,7 +27,6 @@ suite.skip('EditorsObserver', function () { const disposables = new DisposableStore(); - setup(() => { disposables.add(registerTestEditor(TEST_EDITOR_ID, [new SyncDescriptor(TestFileEditorInput)], TEST_SERIALIZABLE_EDITOR_INPUT_ID)); disposables.add(registerTestSideBySideEditor()); @@ -68,7 +68,7 @@ suite.skip('EditorsObserver', function () { const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); - await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input1, { pinned: true }); currentEditorsMRU = observer.editors; assert.strictEqual(currentEditorsMRU.length, 1); @@ -85,8 +85,8 @@ suite.skip('EditorsObserver', function () { assert.strictEqual(observer.hasEditors(input2.resource), false); assert.strictEqual(observer.hasEditor({ resource: input2.resource, typeId: input2.typeId }), false); - await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); - await part.activeGroup.openEditor(input3, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input2, { pinned: true }); + await part.activeGroup.openEditor(input3, { pinned: true }); currentEditorsMRU = observer.editors; assert.strictEqual(currentEditorsMRU.length, 3); @@ -99,7 +99,7 @@ suite.skip('EditorsObserver', function () { assert.strictEqual(observer.hasEditor({ resource: input2.resource, typeId: input2.typeId }), true); assert.strictEqual(observer.hasEditor({ resource: input3.resource, typeId: input3.typeId }), true); - await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input2, { pinned: true }); currentEditorsMRU = observer.editors; assert.strictEqual(currentEditorsMRU.length, 3); @@ -149,8 +149,8 @@ suite.skip('EditorsObserver', function () { const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE })); - await sideGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE })); + await rootGroup.openEditor(input1, { pinned: true, activation: EditorActivation.ACTIVATE }); + await sideGroup.openEditor(input1, { pinned: true, activation: EditorActivation.ACTIVATE }); currentEditorsMRU = observer.editors; assert.strictEqual(currentEditorsMRU.length, 2); @@ -161,7 +161,7 @@ suite.skip('EditorsObserver', function () { assert.strictEqual(observer.hasEditors(input1.resource), true); assert.strictEqual(observer.hasEditor({ resource: input1.resource, typeId: input1.typeId }), true); - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE })); + await rootGroup.openEditor(input1, { pinned: true, activation: EditorActivation.ACTIVATE }); currentEditorsMRU = observer.editors; assert.strictEqual(currentEditorsMRU.length, 2); @@ -176,7 +176,7 @@ suite.skip('EditorsObserver', function () { // the most recent editor, but rather put it behind const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); - await rootGroup.openEditor(input2, EditorOptions.create({ inactive: true })); + await rootGroup.openEditor(input2, { inactive: true }); currentEditorsMRU = observer.editors; assert.strictEqual(currentEditorsMRU.length, 3); @@ -222,13 +222,13 @@ suite.skip('EditorsObserver', function () { assert.strictEqual(observer.hasEditor({ resource: input1.resource, typeId: input1.typeId }), false); assert.strictEqual(observer.hasEditor({ resource: input2.resource, typeId: input2.typeId }), false); - await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input1, { pinned: true }); assert.strictEqual(observer.hasEditors(input1.resource), true); assert.strictEqual(observer.hasEditor({ resource: input1.resource, typeId: input1.typeId }), true); assert.strictEqual(observer.hasEditor({ resource: input2.resource, typeId: input2.typeId }), false); - await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input2, { pinned: true }); assert.strictEqual(observer.hasEditors(input1.resource), true); assert.strictEqual(observer.hasEditor({ resource: input1.resource, typeId: input1.typeId }), true); @@ -259,13 +259,13 @@ suite.skip('EditorsObserver', function () { assert.strictEqual(observer.hasEditor({ resource: primary.resource, typeId: primary.typeId }), false); assert.strictEqual(observer.hasEditor({ resource: secondary.resource, typeId: secondary.typeId }), false); - await part.activeGroup.openEditor(input, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input, { pinned: true }); assert.strictEqual(observer.hasEditors(primary.resource), true); assert.strictEqual(observer.hasEditor({ resource: primary.resource, typeId: primary.typeId }), true); assert.strictEqual(observer.hasEditor({ resource: secondary.resource, typeId: secondary.typeId }), false); - await part.activeGroup.openEditor(primary, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(primary, { pinned: true }); assert.strictEqual(observer.hasEditors(primary.resource), true); assert.strictEqual(observer.hasEditor({ resource: primary.resource, typeId: primary.typeId }), true); @@ -293,9 +293,9 @@ suite.skip('EditorsObserver', function () { const rootGroup = part.activeGroup; - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input1, { pinned: true }); + await rootGroup.openEditor(input2, { pinned: true }); + await rootGroup.openEditor(input3, { pinned: true }); let currentEditorsMRU = observer.editors; assert.strictEqual(currentEditorsMRU.length, 3); @@ -353,9 +353,9 @@ suite.skip('EditorsObserver', function () { const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input1, { pinned: true }); + await rootGroup.openEditor(input2, { pinned: true }); + await rootGroup.openEditor(input3, { pinned: true }); const storage = new TestStorageService(); const observer = disposables.add(new EditorsObserver(part, storage)); @@ -400,11 +400,11 @@ suite.skip('EditorsObserver', function () { const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input1, { pinned: true }); + await rootGroup.openEditor(input2, { pinned: true }); const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); - await sideGroup.openEditor(input3, EditorOptions.create({ pinned: true })); + await sideGroup.openEditor(input3, { pinned: true }); const storage = new TestStorageService(); const observer = disposables.add(new EditorsObserver(part, storage)); @@ -447,7 +447,7 @@ suite.skip('EditorsObserver', function () { const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input1, { pinned: true }); const storage = new TestStorageService(); const observer = disposables.add(new EditorsObserver(part, storage)); @@ -484,10 +484,10 @@ suite.skip('EditorsObserver', function () { const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID); const input4 = new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID); - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input4, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input1, { pinned: true }); + await rootGroup.openEditor(input2, { pinned: true }); + await rootGroup.openEditor(input3, { pinned: true }); + await rootGroup.openEditor(input4, { pinned: true }); assert.strictEqual(rootGroup.count, 3); assert.strictEqual(rootGroup.contains(input1), false); @@ -515,7 +515,7 @@ suite.skip('EditorsObserver', function () { assert.strictEqual(observer.hasEditor({ resource: input4.resource, typeId: input4.typeId }), true); const input5 = new TestFileEditorInput(URI.parse('foo://bar5'), TEST_EDITOR_INPUT_ID); - await sideGroup.openEditor(input5, EditorOptions.create({ pinned: true })); + await sideGroup.openEditor(input5, { pinned: true }); assert.strictEqual(rootGroup.count, 1); assert.strictEqual(rootGroup.contains(input1), false); @@ -545,10 +545,10 @@ suite.skip('EditorsObserver', function () { const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID); const input4 = new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID); - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input4, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input1, { pinned: true }); + await rootGroup.openEditor(input2, { pinned: true }); + await rootGroup.openEditor(input3, { pinned: true }); + await rootGroup.openEditor(input4, { pinned: true }); assert.strictEqual(rootGroup.count, 3); // 1 editor got closed due to our limit! assert.strictEqual(rootGroup.contains(input1), false); @@ -560,10 +560,10 @@ suite.skip('EditorsObserver', function () { assert.strictEqual(observer.hasEditor({ resource: input3.resource, typeId: input3.typeId }), true); assert.strictEqual(observer.hasEditor({ resource: input4.resource, typeId: input4.typeId }), true); - await sideGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - await sideGroup.openEditor(input2, EditorOptions.create({ pinned: true })); - await sideGroup.openEditor(input3, EditorOptions.create({ pinned: true })); - await sideGroup.openEditor(input4, EditorOptions.create({ pinned: true })); + await sideGroup.openEditor(input1, { pinned: true }); + await sideGroup.openEditor(input2, { pinned: true }); + await sideGroup.openEditor(input3, { pinned: true }); + await sideGroup.openEditor(input4, { pinned: true }); assert.strictEqual(sideGroup.count, 3); assert.strictEqual(sideGroup.contains(input1), false); @@ -611,10 +611,10 @@ suite.skip('EditorsObserver', function () { const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID); const input4 = new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID); - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true, sticky: true })); - await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input4, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input1, { pinned: true, sticky: true }); + await rootGroup.openEditor(input2, { pinned: true }); + await rootGroup.openEditor(input3, { pinned: true }); + await rootGroup.openEditor(input4, { pinned: true }); assert.strictEqual(rootGroup.count, 3); assert.strictEqual(rootGroup.contains(input1), true); diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index dc083aabbe..0a9fea800d 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -226,25 +226,15 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment get disableExtensions() { return this.payload?.get('disableExtensions') === 'true'; } - private get webviewEndpoint(): string { - // TODO@matt: get fallback from product service - return this.options.webviewEndpoint || 'https://{{uuid}}.vscode-webview-test.com/{{commit}}'; - } - @memoize get webviewExternalEndpoint(): string { - return (this.webviewEndpoint).replace('{{commit}}', this.productService.commit || '23a2409675bc1bde94f3532bc7c5826a6e99e4b6'); - } + const endpoint = this.options.webviewEndpoint + || this.productService.webviewContentExternalBaseUrlTemplate + || 'https://{{uuid}}.vscode-webview.net/{{quality}}/{{commit}}/out/vs/workbench/contrib/webview/browser/pre/'; - @memoize - get webviewResourceRoot(): string { - return `${this.webviewExternalEndpoint}/vscode-resource/{{resource}}`; - } - - @memoize - get webviewCspSource(): string { - const uri = URI.parse(this.webviewEndpoint.replace('{{uuid}}', '*')); - return `${uri.scheme}://${uri.authority}`; + return endpoint + .replace('{{commit}}', this.payload?.get('webviewExternalEndpointCommit') ?? this.productService.commit ?? '97740a7d253650f9f186c211de5247e2577ce9f7') + .replace('{{quality}}', this.productService.quality || 'insider'); } @memoize @@ -256,6 +246,9 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment get skipReleaseNotes(): boolean { return false; } + @memoize + get disableWorkspaceTrust(): boolean { return true; } + private payload: Map | undefined; constructor( diff --git a/src/vs/workbench/services/environment/common/environmentService.ts b/src/vs/workbench/services/environment/common/environmentService.ts index dd6d236eb4..e0a925eab8 100644 --- a/src/vs/workbench/services/environment/common/environmentService.ts +++ b/src/vs/workbench/services/environment/common/environmentService.ts @@ -36,8 +36,6 @@ export interface IWorkbenchEnvironmentService extends IEnvironmentService { readonly extensionEnabledProposedApi?: string[]; readonly webviewExternalEndpoint: string; - readonly webviewResourceRoot: string; - readonly webviewCspSource: string; readonly skipReleaseNotes: boolean; diff --git a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts index e9c2773838..b577939a5c 100644 --- a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts +++ b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts @@ -68,21 +68,6 @@ export class NativeWorkbenchEnvironmentService extends AbstractNativeEnvironment @memoize get webviewExternalEndpoint(): string { return `${Schemas.vscodeWebview}://{{uuid}}`; } - @memoize - get webviewResourceRoot(): string { - // On desktop, this endpoint is only used for the service worker to identify resource loads and - // should never actually be requested. - // - // Required due to https://github.com/electron/electron/issues/28528 - return 'https://{{uuid}}.vscode-webview-test.com/vscode-resource/{{resource}}'; - } - - @memoize - get webviewCspSource(): string { - const uri = URI.parse(this.webviewResourceRoot.replace('{{uuid}}', '*')); - return `${uri.scheme}://${uri.authority}`; - } - @memoize get skipReleaseNotes(): boolean { return !!this.args['skip-release-notes']; } diff --git a/src/vs/workbench/services/environment/electron-sandbox/shellEnvironmentService.ts b/src/vs/workbench/services/environment/electron-sandbox/shellEnvironmentService.ts index a0c728b7c2..5c36538279 100644 --- a/src/vs/workbench/services/environment/electron-sandbox/shellEnvironmentService.ts +++ b/src/vs/workbench/services/environment/electron-sandbox/shellEnvironmentService.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionBisect.ts b/src/vs/workbench/services/extensionManagement/browser/extensionBisect.ts index 780454cf83..fc4d3622f8 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionBisect.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionBisect.ts @@ -291,7 +291,7 @@ registerAction2(class extends Action2 { if (done.bad) { // DONE but nothing found - await dialogService.show(Severity.Info, localize('done.msg', "Extension Bisect"), [], { + await dialogService.show(Severity.Info, localize('done.msg', "Extension Bisect"), undefined, { detail: localize('done.detail2', "Extension Bisect is done but no extension has been identified. This might be a problem with {0}.", productService.nameShort) }); @@ -299,7 +299,6 @@ registerAction2(class extends Action2 { // DONE and identified extension const res = await dialogService.show(Severity.Info, localize('done.msg', "Extension Bisect"), [localize('report', "Report Issue & Continue"), localize('done', "Continue")], - // [], { detail: localize('done.detail', "Extension Bisect is done and has identified {0} as the extension causing the problem.", done.id), checkbox: { label: localize('done.disbale', "Keep this extension disabled"), checked: true }, diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index 6a07524a5a..c1a78fcf3c 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -26,7 +26,7 @@ import { IExtensionBisectService } from 'vs/workbench/services/extensionManageme import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { Promises } from 'vs/base/common/async'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; -import { getVirtualWorkspaceScheme } from 'vs/platform/remote/common/remoteHosts'; +import { isVirtualWorkspace } from 'vs/platform/remote/common/remoteHosts'; const SOURCE = 'IWorkbenchExtensionEnablementService'; @@ -95,7 +95,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench if (this._isDisabledByExtensionKind(extension)) { return EnablementState.DisabledByExtensionKind; } - if (this._isEnabled(extension) && this._isDisabledByTrustRequirement(extension)) { + if (this._isEnabled(extension) && this._isDisabledByWorkspaceTrust(extension)) { return EnablementState.DisabledByTrustRequirement; } return this._getEnablementState(extension.identifier); @@ -160,14 +160,10 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } const result = await Promises.settled(extensions.map(e => { - if (this._isDisabledByTrustRequirement(e)) { - return this.workspaceTrustRequestService.requestWorkspaceTrust({ modal: true }) + if (this._isDisabledByWorkspaceTrust(e)) { + return this.workspaceTrustRequestService.requestWorkspaceTrust() .then(trustState => { - if (trustState) { - return this._setEnablement(e, newState); - } else { - return Promise.resolve(false); - } + return Promise.resolve(trustState ?? false); }); } else { return this._setEnablement(e, newState); @@ -233,8 +229,8 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } private _isDisabledByVirtualWorkspace(extension: IExtension): boolean { - if (getVirtualWorkspaceScheme(this.contextService.getWorkspace()) !== undefined) { - return !this.extensionManifestPropertiesService.canSupportVirtualWorkspace(extension.manifest); + if (isVirtualWorkspace(this.contextService.getWorkspace())) { + return this.extensionManifestPropertiesService.getExtensionVirtualWorkspaceSupportType(extension.manifest) === false; } return false; } @@ -272,12 +268,16 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench return false; } - private _isDisabledByTrustRequirement(extension: IExtension): boolean { + isDisabledByWorkspaceTrust(extension: IExtension): boolean { + return this._isEnabled(extension) && this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(extension.manifest) === false; + } + + private _isDisabledByWorkspaceTrust(extension: IExtension): boolean { if (this.workspaceTrustManagementService.isWorkpaceTrusted()) { return false; } - return this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(extension.manifest) === false; + return this.isDisabledByWorkspaceTrust(extension); } private _getEnablementState(identifier: IExtensionIdentifier): EnablementState { @@ -416,7 +416,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } private _onDidInstallExtension({ local, error }: DidInstallExtensionEvent): void { - if (local && !error && this._isDisabledByTrustRequirement(local)) { + if (local && !error && this._isDisabledByWorkspaceTrust(local)) { this._onEnablementChanged.fire([local]); } } @@ -427,15 +427,12 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } } - private async _getExtensionsByWorkspaceTrustRequirement(): Promise { - const extensions = await this.extensionManagementService.getInstalled(); - return extensions.filter(e => this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(e.manifest) === false); - } - public async updateEnablementByWorkspaceTrustRequirement(): Promise { - const extensions = await this._getExtensionsByWorkspaceTrustRequirement(); - if (extensions.length) { - this._onEnablementChanged.fire(extensions); + const installedExtensions = await this.extensionManagementService.getInstalled(); + const disabledExtensions = installedExtensions.filter(e => this.isDisabledByWorkspaceTrust(e)); + + if (disabledExtensions.length) { + this._onEnablementChanged.fire(disabledExtensions); } } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 76bf4ee023..5b75866f08 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -9,7 +9,6 @@ import { IExtension, IScannedExtension, ExtensionType, ITranslatedScannedExtensi import { IExtensionManagementService, IGalleryExtension, IExtensionIdentifier, ILocalExtension, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { IWorkspace, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; // {{SQL CARBON EDIT}} -import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; export interface IExtensionManagementServer { id: string; @@ -83,6 +82,12 @@ export interface IWorkbenchExtensionEnablementService { */ isDisabledGlobally(extension: IExtension): boolean; + /** + * Returns `true` if the given extension identifier is enabled by the user but it it + * disabled due to the fact that the current window/folder/workspace is not trusted. + */ + isDisabledByWorkspaceTrust(extension: IExtension): boolean; + /** * Enable or disable the given extension. * if `workspace` is `true` then enablement is done for workspace, otherwise globally. @@ -101,16 +106,12 @@ export interface IWorkbenchExtensionEnablementService { updateEnablementByWorkspaceTrustRequirement(): Promise; } +// {{SQL CARBON EDIT}} export interface IExtensionsConfigContent { recommendations: string[]; unwantedRecommendations: string[]; } -export type RecommendationChangeNotification = { - extensionId: string, - isRecommended: boolean -}; - export type DynamicRecommendation = 'dynamic'; export type ConfigRecommendation = 'config'; export type ExecutableRecommendation = 'executable'; @@ -123,12 +124,7 @@ export interface IExtensionRecommendation { extensionId: string; sources: ExtensionRecommendationSource[]; } - -export interface IExtensionRecommendationReason { - reasonId: ExtensionRecommendationReason; - reasonText: string; -} - +// {{SQL CARBON EDIT}} - End export const IWebExtensionsScannerService = createDecorator('IWebExtensionsScannerService'); export interface IWebExtensionsScannerService { readonly _serviceBrand: undefined; diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index cf84660b47..5a4778390d 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -5,7 +5,7 @@ import { Event, EventMultiplexer } from 'vs/base/common/event'; import { - ILocalExtension, IGalleryExtension, InstallExtensionEvent, DidInstallExtensionEvent, IExtensionIdentifier, DidUninstallExtensionEvent, IReportedExtension, IGalleryMetadata, IExtensionGalleryService, InstallOptions, UninstallOptions, INSTALL_ERROR_NOT_SUPPORTED + ILocalExtension, IGalleryExtension, InstallExtensionEvent, DidInstallExtensionEvent, IExtensionIdentifier, DidUninstallExtensionEvent, IReportedExtension, IGalleryMetadata, IExtensionGalleryService, InstallOptions, UninstallOptions, INSTALL_ERROR_NOT_SUPPORTED, InstallVSIXOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionManagementServer, IExtensionManagementServerService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionType, isLanguagePackExtension, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; @@ -171,35 +171,35 @@ export class ExtensionManagementService extends Disposable implements IWorkbench .map(({ extensionManagementService }) => extensionManagementService.unzip(zipLocation))).then(([extensionIdentifier]) => extensionIdentifier); } - async install(vsix: URI): Promise { + async install(vsix: URI, options?: InstallVSIXOptions): Promise { if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) { const manifest = await this.getManifest(vsix); if (isLanguagePackExtension(manifest)) { // Install on both servers - const [local] = await Promises.settled([this.extensionManagementServerService.localExtensionManagementServer, this.extensionManagementServerService.remoteExtensionManagementServer].map(server => this.installVSIX(vsix, server))); + const [local] = await Promises.settled([this.extensionManagementServerService.localExtensionManagementServer, this.extensionManagementServerService.remoteExtensionManagementServer].map(server => this.installVSIX(vsix, server, options))); return local; } if (this.extensionManifestPropertiesService.prefersExecuteOnUI(manifest)) { // Install only on local server - return this.installVSIX(vsix, this.extensionManagementServerService.localExtensionManagementServer); + return this.installVSIX(vsix, this.extensionManagementServerService.localExtensionManagementServer, options); } // Install only on remote server - return this.installVSIX(vsix, this.extensionManagementServerService.remoteExtensionManagementServer); + return this.installVSIX(vsix, this.extensionManagementServerService.remoteExtensionManagementServer, options); } if (this.extensionManagementServerService.localExtensionManagementServer) { - return this.installVSIX(vsix, this.extensionManagementServerService.localExtensionManagementServer); + return this.installVSIX(vsix, this.extensionManagementServerService.localExtensionManagementServer, options); } if (this.extensionManagementServerService.remoteExtensionManagementServer) { - return this.installVSIX(vsix, this.extensionManagementServerService.remoteExtensionManagementServer); + return this.installVSIX(vsix, this.extensionManagementServerService.remoteExtensionManagementServer, options); } return Promise.reject('No Servers to Install'); } - protected async installVSIX(vsix: URI, server: IExtensionManagementServer): Promise { + protected async installVSIX(vsix: URI, server: IExtensionManagementServer, options: InstallVSIXOptions | undefined): Promise { const manifest = await this.getManifest(vsix); if (manifest) { await this.checkForWorkspaceTrust(manifest); - return server.extensionManagementService.install(vsix); + return server.extensionManagementService.install(vsix, options); } return Promise.reject('Unable to get the extension manifest.'); } @@ -360,7 +360,6 @@ export class ExtensionManagementService extends Disposable implements IWorkbench protected async checkForWorkspaceTrust(manifest: IExtensionManifest): Promise { if (this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(manifest) === false) { const trustState = await this.workspaceTrustRequestService.requestWorkspaceTrust({ - modal: true, message: localize('extensionInstallWorkspaceTrustMessage', "Enabling this extension requires a trusted workspace."), buttons: [ { label: localize('extensionInstallWorkspaceTrustButton', "Trust Workspace & Install"), type: 'ContinueWithTrust' }, diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts index a00fd8d092..11d59e38b1 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts @@ -21,11 +21,10 @@ import { groupByExtension, areSameExtensions, getGalleryExtensionId } from 'vs/p import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import type { IStaticExtension } from 'vs/workbench/workbench.web.api'; import { Disposable } from 'vs/base/common/lifecycle'; -import { Event } from 'vs/base/common/event'; import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; import { localize } from 'vs/nls'; import * as semver from 'vs/base/common/semver/semver'; -import { isArray } from 'vs/base/common/types'; +import { isArray, isFunction } from 'vs/base/common/types'; interface IUserExtension { identifier: IExtensionIdentifier; @@ -54,8 +53,6 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten private readonly extensionsResource: URI | undefined = undefined; private readonly userExtensionsResourceLimiter: Queue = new Queue(); - private userExtensionsPromise: Promise | undefined; - constructor( @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IBuiltinExtensionsScannerService private readonly builtinExtensionsScannerService: IBuiltinExtensionsScannerService, @@ -69,15 +66,20 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten this.extensionsResource = joinPath(environmentService.userRoamingDataHome, 'extensions.json'); this.systemExtensionsPromise = this.readSystemExtensions(); this.defaultExtensionsPromise = this.readDefaultExtensions(); - if (this.extensionsResource) { - this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.extensionsResource!))(() => this.userExtensionsPromise = undefined)); - } } } private async readSystemExtensions(): Promise { - const extensions = await this.builtinExtensionsScannerService.scanBuiltinExtensions(); - return extensions.concat(this.getStaticExtensions(true)); + let [builtinExtensions, staticExtensions] = await Promise.all([ + this.builtinExtensionsScannerService.scanBuiltinExtensions(), + this.getStaticExtensions(true) + ]); + + if (isFunction(this.environmentService.options?.builtinExtensionsFilter)) { + builtinExtensions = builtinExtensions.filter(e => this.environmentService.options!.builtinExtensionsFilter!(e.identifier.id)); + } + + return [...builtinExtensions, ...staticExtensions]; } /** @@ -183,10 +185,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten if (type === undefined || type === ExtensionType.User) { const staticExtensions = await this.defaultExtensionsPromise; extensions.push(...staticExtensions); - if (!this.userExtensionsPromise) { - this.userExtensionsPromise = this.scanUserExtensions(); - } - const userExtensions = await this.userExtensionsPromise; + const userExtensions = await this.scanUserExtensions(); extensions.push(...userExtensions); } return extensions; @@ -259,6 +258,10 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten } canAddExtension(galleryExtension: IGalleryExtension): boolean { + if (this.environmentService.options?.assumeGalleryExtensionsAreAddressable) { + return true; + } + return !!galleryExtension.properties.webExtension && !!galleryExtension.webResource; } @@ -267,7 +270,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten throw new Error(localize('cannot be installed', "Cannot install '{0}' because this extension is not a web extension.", galleryExtension.displayName || galleryExtension.name)); } - const extensionLocation = galleryExtension.webResource!; + const extensionLocation = joinPath(galleryExtension.assetUri, 'Microsoft.VisualStudio.Code.WebResources', 'extension'); const packageNLSUri = joinPath(extensionLocation, 'package.nls.json'); const context = await this.requestService.request({ type: 'GET', url: packageNLSUri.toString() }, CancellationToken.None); const packageNLSExists = isSuccess(context); @@ -371,7 +374,6 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten packageNLSUri: e.packageNLSUri?.toJSON(), })); await this.fileService.writeFile(this.extensionsResource!, VSBuffer.fromString(JSON.stringify(storedUserExtensions))); - this.userExtensionsPromise = undefined; return userExtensions; }); } diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts index fc14db15f2..d9373fc586 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { generateUuid } from 'vs/base/common/uuid'; -import { ILocalExtension, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ILocalExtension, IExtensionGalleryService, InstallVSIXOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { ExtensionManagementService as BaseExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagementService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -38,7 +38,7 @@ export class ExtensionManagementService extends BaseExtensionManagementService { super(extensionManagementServerService, extensionGalleryService, configurationService, productService, downloadService, userDataAutoSyncEnablementService, userDataSyncResourceEnablementService, dialogService, workspaceTrustRequestService, extensionManifestPropertiesService); } - protected override async installVSIX(vsix: URI, server: IExtensionManagementServer): Promise { + protected override async installVSIX(vsix: URI, server: IExtensionManagementServer, options: InstallVSIXOptions | undefined): Promise { if (vsix.scheme === Schemas.vscodeRemote && server === this.extensionManagementServerService.localExtensionManagementServer) { const downloadedLocation = joinPath(this.environmentService.tmpDir, generateUuid()); await this.downloadService.download(vsix, downloadedLocation); @@ -47,7 +47,7 @@ export class ExtensionManagementService extends BaseExtensionManagementService { const manifest = await this.getManifest(vsix); if (manifest) { await this.checkForWorkspaceTrust(manifest); - return server.extensionManagementService.install(vsix); + return server.extensionManagementService.install(vsix, options); } return Promise.reject('Unable to get the extension manifest.'); diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts index a680ec0232..863c619170 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionManagementService, ILocalExtension, IGalleryExtension, IExtensionGalleryService, InstallOperation, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, ILocalExtension, IGalleryExtension, IExtensionGalleryService, InstallOperation, InstallOptions, InstallVSIXOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -41,8 +41,8 @@ export class NativeRemoteExtensionManagementService extends WebRemoteExtensionMa this.localExtensionManagementService = localExtensionManagementServer.extensionManagementService; } - override async install(vsix: URI): Promise { - const local = await super.install(vsix); + override async install(vsix: URI, options?: InstallVSIXOptions): Promise { + const local = await super.install(vsix, options); await this.installUIDependenciesAndPackedExtensions(local); return local; } diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index 6e2cff9aa6..fbf6cf1f0a 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -55,6 +55,7 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { const storageService = createStorageService(instantiationService); const extensionManagementService = instantiationService.get(IExtensionManagementService) || instantiationService.stub(IExtensionManagementService, { onDidInstallExtension: new Emitter().event, onDidUninstallExtension: new Emitter().event } as IExtensionManagementService); const extensionManagementServerService = instantiationService.get(IExtensionManagementServerService) || instantiationService.stub(IExtensionManagementServerService, { localExtensionManagementServer: { extensionManagementService } }); + const workspaceTrustManagementService = instantiationService.get(IWorkspaceTrustManagementService) || instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); super( storageService, new GlobalExtensionEnablementService(storageService), @@ -69,9 +70,9 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { instantiationService.get(INotificationService) || instantiationService.stub(INotificationService, new TestNotificationService()), instantiationService.get(IHostService), new class extends mock() { override isDisabledByBisect() { return false; } }, - instantiationService.get(IWorkspaceTrustManagementService) || instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()), + workspaceTrustManagementService, new class extends mock() { override requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise { return Promise.resolve(true); } }, - instantiationService.get(IExtensionManifestPropertiesService) || instantiationService.stub(IExtensionManifestPropertiesService, new ExtensionManifestPropertiesService(TestProductService, new TestConfigurationService())) + instantiationService.get(IExtensionManifestPropertiesService) || instantiationService.stub(IExtensionManifestPropertiesService, new ExtensionManifestPropertiesService(TestProductService, new TestConfigurationService(), workspaceTrustManagementService)) ); } diff --git a/src/vs/workbench/services/extensionRecommendations/common/extensionRecommendations.ts b/src/vs/workbench/services/extensionRecommendations/common/extensionRecommendations.ts index dc3535cd60..a1b6f0d308 100644 --- a/src/vs/workbench/services/extensionRecommendations/common/extensionRecommendations.ts +++ b/src/vs/workbench/services/extensionRecommendations/common/extensionRecommendations.ts @@ -48,6 +48,7 @@ export interface IExtensionRecommendationsService { getRecommendedExtensionsByScenario(scenarioType: string): Promise; // {{SQL CARBON EDIT}} promptRecommendedExtensionsByScenario(scenarioType: string): void; // {{SQL CARBON EDIT}} + getLanguageRecommendations(): string[]; } export type IgnoredRecommendationChangeNotification = { diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index 7db6da2a68..473f3e6d1b 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -185,8 +185,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._remoteAgentService.getEnvironment(), this._remoteAgentService.scanExtensions() ]); - localExtensions = this._checkEnabledAndProposedAPI(localExtensions); - remoteExtensions = this._checkEnabledAndProposedAPI(remoteExtensions); + localExtensions = this._checkEnabledAndProposedAPI(localExtensions, false); + remoteExtensions = this._checkEnabledAndProposedAPI(remoteExtensions, false); const remoteAgentConnection = this._remoteAgentService.getConnection(); this._runningLocation = this._runningLocationClassifier.determineRunningLocation(localExtensions, remoteExtensions); diff --git a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts index 80035e46fb..2767fd0b34 100644 --- a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts +++ b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts @@ -8,7 +8,7 @@ import { IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/li import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -263,7 +263,13 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { // Extension is not installed else { - const galleryExtension = await this.galleryService.getCompatibleExtension(extensionIdentifier); + let galleryExtension: IGalleryExtension | undefined; + + try { + galleryExtension = await this.galleryService.getCompatibleExtension(extensionIdentifier) ?? undefined; + } catch (err) { + return; + } if (!galleryExtension) { return; @@ -285,7 +291,7 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { await this.progressService.withProgress({ location: ProgressLocation.Notification, title: localize('Installing', "Installing Extension '{0}'...", galleryExtension.displayName || galleryExtension.name) - }, () => this.extensionManagementService.installFromGallery(galleryExtension)); + }, () => this.extensionManagementService.installFromGallery(galleryExtension!)); this.notificationService.prompt( Severity.Info, diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts index da6c1d952b..b75666c768 100644 --- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts @@ -361,8 +361,6 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI, globalStorageHome: this._environmentService.globalStorageHome, workspaceStorageHome: this._environmentService.workspaceStorageHome, - webviewResourceRoot: this._environmentService.webviewResourceRoot, - webviewCspSource: this._environmentService.webviewCspSource, }, workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : { configuration: workspace.configuration || undefined, diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 72e0184587..31065c1af1 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { Barrier } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import * as perf from 'vs/base/common/performance'; import { isEqualOrParent } from 'vs/base/common/resources'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -51,7 +51,7 @@ export function parseScannedExtension(extension: ITranslatedScannedExtension): I class DeltaExtensionsQueueItem { constructor( public readonly toAdd: IExtension[], - public readonly toRemove: string[] + public readonly toRemove: string[] | IExtension[] ) { } } @@ -68,6 +68,69 @@ export const enum ExtensionRunningPreference { Remote } +class LockCustomer { + public readonly promise: Promise; + private _resolve!: (value: IDisposable) => void; + + constructor( + public readonly name: string + ) { + this.promise = new Promise((resolve, reject) => { + this._resolve = resolve; + }); + } + + resolve(value: IDisposable): void { + this._resolve(value); + } +} + +class Lock { + private readonly _pendingCustomers: LockCustomer[] = []; + private _isLocked = false; + + public async acquire(customerName: string): Promise { + const customer = new LockCustomer(customerName); + this._pendingCustomers.push(customer); + this._advance(); + return customer.promise; + } + + private _advance(): void { + if (this._isLocked) { + // cannot advance yet + return; + } + if (this._pendingCustomers.length === 0) { + // no more waiting customers + return; + } + + const customer = this._pendingCustomers.shift()!; + + this._isLocked = true; + let customerHoldsLock = true; + + let logLongRunningCustomerTimeout = setTimeout(() => { + if (customerHoldsLock) { + console.warn(`The customer named ${customer.name} has been holding on to the lock for 30s. This might be a problem.`); + } + }, 30 * 1000 /* 30 seconds */); + + const releaseLock = () => { + if (!customerHoldsLock) { + return; + } + clearTimeout(logLongRunningCustomerTimeout); + customerHoldsLock = false; + this._isLocked = false; + this._advance(); + }; + + customer.resolve(toDisposable(releaseLock)); + } +} + export abstract class AbstractExtensionService extends Disposable implements IExtensionService { public _serviceBrand: undefined; @@ -88,6 +151,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx public readonly onDidChangeResponsiveChange: Event = this._onDidChangeResponsiveChange.event; protected readonly _registry: ExtensionDescriptionRegistry; + private readonly _registryLock: Lock; + private readonly _installedExtensionsReady: Barrier; protected readonly _isDev: boolean; private readonly _extensionsMessages: Map; @@ -98,7 +163,6 @@ export abstract class AbstractExtensionService extends Disposable implements IEx private _deltaExtensionsQueue: DeltaExtensionsQueueItem[]; private _inHandleDeltaExtensions: boolean; - private readonly _onDidFinishHandleDeltaExtensions = this._register(new Emitter()); protected _runningLocation: Map; @@ -130,6 +194,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx })); this._registry = new ExtensionDescriptionRegistry([]); + this._registryLock = new Lock(); + this._installedExtensionsReady = new Barrier(); this._isDev = !this._environmentService.isBuilt || this._environmentService.isExtensionDevelopment; this._extensionsMessages = new Map(); @@ -151,14 +217,14 @@ export abstract class AbstractExtensionService extends Disposable implements IEx this._register(this._extensionEnablementService.onEnablementChanged((extensions) => { let toAdd: IExtension[] = []; - let toRemove: string[] = []; + let toRemove: IExtension[] = []; for (const extension of extensions) { if (this._safeInvokeIsEnabled(extension)) { // an extension has been enabled toAdd.push(extension); } else { // an extension has been disabled - toRemove.push(extension.identifier.id); + toRemove.push(extension); } } this._handleDeltaExtensions(new DeltaExtensionsQueueItem(toAdd, toRemove)); @@ -207,20 +273,23 @@ export abstract class AbstractExtensionService extends Disposable implements IEx return; } - while (this._deltaExtensionsQueue.length > 0) { - const item = this._deltaExtensionsQueue.shift()!; - try { - this._inHandleDeltaExtensions = true; + let lock: IDisposable | null = null; + try { + this._inHandleDeltaExtensions = true; + lock = await this._registryLock.acquire('handleDeltaExtensions'); + while (this._deltaExtensionsQueue.length > 0) { + const item = this._deltaExtensionsQueue.shift()!; await this._deltaExtensions(item.toAdd, item.toRemove); - } finally { - this._inHandleDeltaExtensions = false; + } + } finally { + this._inHandleDeltaExtensions = false; + if (lock) { + lock.dispose(); } } - - this._onDidFinishHandleDeltaExtensions.fire(); } - private async _deltaExtensions(_toAdd: IExtension[], _toRemove: string[]): Promise { + private async _deltaExtensions(_toAdd: IExtension[], _toRemove: string[] | IExtension[]): Promise { let toAdd: IExtensionDescription[] = []; for (let i = 0, len = _toAdd.length; i < len; i++) { const extension = _toAdd[i]; @@ -240,13 +309,20 @@ export abstract class AbstractExtensionService extends Disposable implements IEx let toRemove: IExtensionDescription[] = []; for (let i = 0, len = _toRemove.length; i < len; i++) { - const extensionId = _toRemove[i]; + const extensionOrId = _toRemove[i]; + const extensionId = (typeof extensionOrId === 'string' ? extensionOrId : extensionOrId.identifier.id); + const extension = (typeof extensionOrId === 'string' ? null : extensionOrId); const extensionDescription = this._registry.getExtensionDescription(extensionId); if (!extensionDescription) { // ignore disabling/uninstalling an extension which is not running continue; } + if (extension && extensionDescription.extensionLocation.scheme !== extension.location.scheme) { + // this event is for a different extension than mine (maybe for the local extension, while I have the remote extension) + continue; + } + if (!this.canRemoveExtension(extensionDescription)) { // uses non-dynamic extension point or is activated continue; @@ -559,11 +635,17 @@ export abstract class AbstractExtensionService extends Disposable implements IEx public async startExtensionHosts(): Promise { this.stopExtensionHosts(); - if (this._inHandleDeltaExtensions) { - await Event.toPromise(this._onDidFinishHandleDeltaExtensions.event); - } + const lock = await this._registryLock.acquire('startExtensionHosts'); + try { + this._startExtensionHosts(false, Array.from(this._allRequestedActivateEvents.keys())); - this._startExtensionHosts(false, Array.from(this._allRequestedActivateEvents.keys())); + const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess); + if (localProcessExtensionHost) { + await localProcessExtensionHost.ready(); + } + } finally { + lock.dispose(); + } } public async restartExtensionHost(): Promise { @@ -680,15 +762,23 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } } - protected _checkEnabledAndProposedAPI(extensions: IExtensionDescription[]): IExtensionDescription[] { + /** + * @argument extensions The extensions to be checked. + * @argument ignoreWorkspaceTrust Do not take workspace trust into account. + */ + protected _checkEnabledAndProposedAPI(extensions: IExtensionDescription[], ignoreWorkspaceTrust: boolean): IExtensionDescription[] { // enable or disable proposed API per extension this._checkEnableProposedApi(extensions); // keep only enabled extensions - return extensions.filter(extension => this._isEnabled(extension)); + return extensions.filter(extension => this._isEnabled(extension, ignoreWorkspaceTrust)); } - protected _isEnabled(extension: IExtensionDescription): boolean { + /** + * @argument extension The extension to be checked. + * @argument ignoreWorkspaceTrust Do not take workspace trust into account. + */ + protected _isEnabled(extension: IExtensionDescription, ignoreWorkspaceTrust: boolean): boolean { if (extension.isUnderDevelopment) { // Never disable extensions under development return true; @@ -699,7 +789,20 @@ export abstract class AbstractExtensionService extends Disposable implements IEx return false; } - return this._safeInvokeIsEnabled(toExtension(extension)); + const ext = toExtension(extension); + + const isEnabled = this._safeInvokeIsEnabled(ext); + if (isEnabled) { + return true; + } + + if (ignoreWorkspaceTrust && this._safeInvokeIsDisabledByWorkspaceTrust(ext)) { + // This extension is disabled, but the reason for it being disabled + // is workspace trust, so we will consider it enabled + return true; + } + + return false; } protected _safeInvokeIsEnabled(extension: IExtension): boolean { @@ -710,6 +813,14 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } } + protected _safeInvokeIsDisabledByWorkspaceTrust(extension: IExtension): boolean { + try { + return this._extensionEnablementService.isDisabledByWorkspaceTrust(extension); + } catch (err) { + return false; + } + } + protected _doHandleExtensionPoints(affectedExtensions: IExtensionDescription[]): void { const affectedExtensionPoints: { [extPointName: string]: boolean; } = Object.create(null); for (let extensionDescription of affectedExtensions) { diff --git a/src/vs/workbench/services/extensions/common/extensionHostMain.ts b/src/vs/workbench/services/extensions/common/extensionHostMain.ts index 25f2aa0dd0..17206805d0 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostMain.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostMain.ts @@ -36,6 +36,7 @@ export class ExtensionHostMain { private _isTerminating: boolean; private readonly _hostUtils: IHostUtils; + private readonly _rpcProtocol: RPCProtocol; private readonly _extensionService: IExtHostExtensionService; private readonly _logService: ILogService; private readonly _disposables = new DisposableStore(); @@ -48,15 +49,15 @@ export class ExtensionHostMain { ) { this._isTerminating = false; this._hostUtils = hostUtils; - const rpcProtocol = new RPCProtocol(protocol, null, uriTransformer); + this._rpcProtocol = new RPCProtocol(protocol, null, uriTransformer); // ensure URIs are transformed and revived - initData = ExtensionHostMain._transform(initData, rpcProtocol); + initData = ExtensionHostMain._transform(initData, this._rpcProtocol); // bootstrap services const services = new ServiceCollection(...getSingletonServiceDescriptors()); services.set(IExtHostInitDataService, { _serviceBrand: undefined, ...initData }); - services.set(IExtHostRpcService, new ExtHostRpcService(rpcProtocol)); + services.set(IExtHostRpcService, new ExtHostRpcService(this._rpcProtocol)); services.set(IURITransformerService, new URITransformerService(uriTransformer)); services.set(IHostUtils, hostUtils); @@ -99,8 +100,8 @@ export class ExtensionHostMain { }; }); - const mainThreadExtensions = rpcProtocol.getProxy(MainContext.MainThreadExtensionService); - const mainThreadErrors = rpcProtocol.getProxy(MainContext.MainThreadErrors); + const mainThreadExtensions = this._rpcProtocol.getProxy(MainContext.MainThreadExtensionService); + const mainThreadErrors = this._rpcProtocol.getProxy(MainContext.MainThreadErrors); errors.setUnexpectedErrorHandler(err => { const data = errors.transformErrorForSerialization(err); const extension = extensionErrors.get(err); @@ -124,9 +125,12 @@ export class ExtensionHostMain { this._disposables.dispose(); errors.setUnexpectedErrorHandler((err) => { - // TODO: write to log once we have one + this._logService.error(err); }); + // Invalidate all proxies + this._rpcProtocol.dispose(); + const extensionsDeactivated = this._extensionService.deactivateAll(); // Give extensions 1 second to wrap up any async dispose, then exit in at most 4 seconds diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index cfb78de09f..68076a3a4e 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -24,6 +24,7 @@ import { IExtensionHost, ExtensionHostKind, ActivationKind } from 'vs/workbench/ import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { timeout } from 'vs/base/common/async'; +import { URI } from 'vs/base/common/uri'; // Enable to see detailed message communication between window and extension host const LOG_EXTENSION_HOST_COMMUNICATION = false; @@ -132,6 +133,10 @@ export class ExtensionHostManager extends Disposable { return p.value; } + public async ready(): Promise { + await this._getProxy(); + } + private async _measureLatency(proxy: ExtHostExtensionServiceShape): Promise { const COUNT = 10; @@ -287,6 +292,15 @@ export class ExtensionHostManager extends Disposable { } } + public async getCanonicalURI(remoteAuthority: string, uri: URI): Promise { + const proxy = await this._getProxy(); + if (!proxy) { + throw new Error(`Cannot resolve canonical URI`); + } + const result = await proxy.$getCanonicalURI(remoteAuthority, uri); + return URI.revive(result); + } + public async start(enabledExtensionIds: ExtensionIdentifier[]): Promise { const proxy = await this._getProxy(); if (!proxy) { diff --git a/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts b/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts index 913b008262..2a69baf7c6 100644 --- a/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts +++ b/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts @@ -1,10 +1,10 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IExtensionManifest, ExtensionKind, ExtensionIdentifier, ExtensionUntrustedWorkpaceSupportType } from 'vs/platform/extensions/common/extensions'; +import { IExtensionManifest, ExtensionKind, ExtensionIdentifier, ExtensionUntrustedWorkpaceSupportType, ExtensionVirtualWorkpaceSupportType } from 'vs/platform/extensions/common/extensions'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { isNonEmptyArray } from 'vs/base/common/arrays'; @@ -13,7 +13,9 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExtensionUntrustedWorkspaceSupport } from 'vs/base/common/product'; import { Disposable } from 'vs/base/common/lifecycle'; -import { isWorkspaceTrustEnabled, WORKSPACE_TRUST_EXTENSION_SUPPORT } from 'vs/workbench/services/workspaces/common/workspaceTrust'; +import { WORKSPACE_TRUST_EXTENSION_SUPPORT } from 'vs/workbench/services/workspaces/common/workspaceTrust'; +import { isBoolean } from 'vs/base/common/types'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; export const IExtensionManifestPropertiesService = createDecorator('extensionManifestPropertiesService'); @@ -30,7 +32,7 @@ export interface IExtensionManifestPropertiesService { getExtensionKind(manifest: IExtensionManifest): ExtensionKind[]; getExtensionUntrustedWorkspaceSupportType(manifest: IExtensionManifest): ExtensionUntrustedWorkpaceSupportType; - canSupportVirtualWorkspace(manifest: IExtensionManifest): boolean; + getExtensionVirtualWorkspaceSupportType(manifest: IExtensionManifest): ExtensionVirtualWorkpaceSupportType; } export class ExtensionManifestPropertiesService extends Disposable implements IExtensionManifestPropertiesService { @@ -50,6 +52,7 @@ export class ExtensionManifestPropertiesService extends Disposable implements IE constructor( @IProductService private readonly productService: IProductService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService ) { super(); @@ -123,7 +126,7 @@ export class ExtensionManifestPropertiesService extends Disposable implements IE getExtensionUntrustedWorkspaceSupportType(manifest: IExtensionManifest): ExtensionUntrustedWorkpaceSupportType { // Workspace trust feature is disabled, or extension has no entry point - if (!isWorkspaceTrustEnabled(this.configurationService) || !manifest.main) { + if (!this.workspaceTrustManagementService.workspaceTrustEnabled || !manifest.main) { return true; } @@ -156,7 +159,7 @@ export class ExtensionManifestPropertiesService extends Disposable implements IE return false; } - canSupportVirtualWorkspace(manifest: IExtensionManifest): boolean { + getExtensionVirtualWorkspaceSupportType(manifest: IExtensionManifest): ExtensionVirtualWorkpaceSupportType { // check user configured const userConfiguredVirtualWorkspaceSupport = this.getConfiguredVirtualWorkspaceSupport(manifest); if (userConfiguredVirtualWorkspaceSupport !== undefined) { @@ -171,8 +174,14 @@ export class ExtensionManifestPropertiesService extends Disposable implements IE } // check the manifest - if (manifest.capabilities?.virtualWorkspaces !== undefined) { - return manifest.capabilities?.virtualWorkspaces; + const virtualWorkspaces = manifest.capabilities?.virtualWorkspaces; + if (isBoolean(virtualWorkspaces)) { + return virtualWorkspaces; + } else if (virtualWorkspaces) { + const supported = virtualWorkspaces.supported; + if (isBoolean(supported) || supported === 'limited') { + return supported; + } } // check default from product diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index 62f923eb99..84c64194d2 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -320,6 +320,16 @@ export const schema: IJSONSchema = { body: 'onAuthenticationRequest:${11:authenticationProviderId}', description: nls.localize('vscode.extension.activationEvents.onAuthenticationRequest', 'An activation event emitted whenever sessions are requested from the specified authentication provider.') }, + { + label: 'onRenderer', + description: nls.localize('vscode.extension.activationEvents.onRenderer', 'An activation event emitted whenever a notebook output renderer is used.'), + body: 'onRenderer:${11:rendererId}' + }, + { + label: 'onTerminalProfile', + body: 'onTerminalProfile:${1:terminalType}', + description: nls.localize('vscode.extension.activationEvents.onTerminalProfile', 'An activation event emitted when a specific terminal profile is launched.'), + }, { label: '*', description: nls.localize('vscode.extension.activationEvents.star', 'An activation event emitted on VS Code startup. To ensure a great end user experience, please use this activation event in your extension only when no other activation events combination works in your use-case.'), @@ -421,8 +431,28 @@ export const schema: IJSONSchema = { properties: { virtualWorkspaces: { description: nls.localize('vscode.extension.capabilities.virtualWorkspaces', "Declares whether the extension should be enabled in virtual workspaces. A virtual workspace is a workspace which is not backed by any on-disk resources. When false, this extension will be automatically disabled in virtual workspaces. Default is true."), - type: 'boolean', - default: true + type: ['boolean', 'object'], + defaultSnippets: [ + { label: 'limited', body: { supported: '${1:limited}', description: '${2}' } }, + { label: 'false', body: { supported: false, description: '${2}' } }, + ], + default: true.valueOf, + properties: { + supported: { + markdownDescription: nls.localize('vscode.extension.capabilities.virtualWorkspaces.supported', "Declares the level of support for virtual workspaces by the extension."), + type: ['string', 'boolean'], + enum: ['limited', true, false], + enumDescriptions: [ + nls.localize('vscode.extension.capabilities.virtualWorkspaces.supported.limited', "The extension will be enabled in virtual workspaces with some functionality disabled."), + nls.localize('vscode.extension.capabilities.virtualWorkspaces.supported.true', "The extension will be enabled in virtual workspaces with all functionality enabled."), + nls.localize('vscode.extension.capabilities.virtualWorkspaces.supported.false', "The extension will not be enabled in virtual workspaces."), + ] + }, + description: { + type: 'string', + markdownDescription: nls.localize('vscode.extension.capabilities.virtualWorkspaces.description', "A description of how virtual workspaces affects the extensions behavior and why it is needed. This only applies when `supported` is not `true`."), + } + } }, untrustedWorkspaces: { description: nls.localize('vscode.extension.capabilities.untrustedWorkspaces', 'Declares how the extension should be handled in untrusted workspaces.'), diff --git a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts index 33fbcd0c46..39beeb0b56 100644 --- a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts +++ b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts @@ -236,9 +236,7 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI, extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI, globalStorageHome: remoteInitData.globalStorageHome, - workspaceStorageHome: remoteInitData.workspaceStorageHome, - webviewResourceRoot: this._environmentService.webviewResourceRoot, - webviewCspSource: this._environmentService.webviewCspSource, + workspaceStorageHome: remoteInitData.workspaceStorageHome }, workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? null : { configuration: workspace.configuration, diff --git a/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts index e138024615..0d9e694e7d 100644 --- a/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts @@ -3,7 +3,6 @@ * 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 errors from 'vs/base/common/errors'; @@ -69,9 +68,10 @@ export class CachedExtensionScanner { const version = this._productService.version; const commit = this._productService.commit; + const date = this._productService.date; const devMode = !!process.env['VSCODE_DEV']; const locale = platform.language; - const input = new ExtensionScannerInput(version, commit, locale, devMode, path, isBuiltin, false, translations); + const input = new ExtensionScannerInput(version, date, commit, locale, devMode, path, isBuiltin, false, translations); return ExtensionScanner.scanSingleExtension(input, log); } @@ -130,7 +130,7 @@ export class CachedExtensionScanner { } try { - await pfs.rimraf(cacheFile, pfs.RimRafMode.MOVE); + await pfs.Promises.rm(cacheFile, pfs.RimRafMode.MOVE); } catch (err) { errors.onUnexpectedError(err); console.error(err); @@ -151,7 +151,7 @@ export class CachedExtensionScanner { const cacheFile = path.join(cacheFolder, cacheKey); try { - const cacheRawContents = await fs.promises.readFile(cacheFile, 'utf8'); + const cacheRawContents = await pfs.Promises.readFile(cacheFile, 'utf8'); return JSON.parse(cacheRawContents); } catch (err) { // That's ok... @@ -165,13 +165,13 @@ export class CachedExtensionScanner { const cacheFile = path.join(cacheFolder, cacheKey); try { - await fs.promises.mkdir(cacheFolder, { recursive: true }); + await pfs.Promises.mkdir(cacheFolder, { recursive: true }); } catch (err) { // That's ok... } try { - await pfs.writeFile(cacheFile, JSON.stringify(cacheContents)); + await pfs.Promises.writeFile(cacheFile, JSON.stringify(cacheContents)); } catch (err) { // That's ok... } @@ -184,7 +184,7 @@ export class CachedExtensionScanner { } try { - const folderStat = await fs.promises.stat(input.absoluteFolderPath); + const folderStat = await pfs.Promises.stat(input.absoluteFolderPath); input.mtime = folderStat.mtime.getTime(); } catch (err) { // That's ok... @@ -224,7 +224,7 @@ export class CachedExtensionScanner { private static async _readTranslationConfig(): Promise { if (platform.translationsConfigFile) { try { - const content = await fs.promises.readFile(platform.translationsConfigFile, 'utf8'); + const content = await pfs.Promises.readFile(platform.translationsConfigFile, 'utf8'); return JSON.parse(content) as Translations; } catch (err) { // no problemo @@ -245,6 +245,7 @@ export class CachedExtensionScanner { const version = productService.version; const commit = productService.commit; + const date = productService.date; const devMode = !!process.env['VSCODE_DEV']; const locale = platform.language; @@ -253,7 +254,7 @@ export class CachedExtensionScanner { notificationService, environmentService, BUILTIN_MANIFEST_CACHE_FILE, - new ExtensionScannerInput(version, commit, locale, devMode, getSystemExtensionsRoot(), true, false, translations), + new ExtensionScannerInput(version, date, commit, locale, devMode, getSystemExtensionsRoot(), true, false, translations), log ); @@ -263,10 +264,10 @@ export class CachedExtensionScanner { const builtInExtensions = Promise.resolve(productService.builtInExtensions || []); const controlFilePath = joinPath(environmentService.userHome, '.vscode-oss-dev', 'extensions', 'control.json').fsPath; - const controlFile = fs.promises.readFile(controlFilePath, 'utf8') + const controlFile = pfs.Promises.readFile(controlFilePath, 'utf8') .then(raw => JSON.parse(raw), () => ({} as any)); - const input = new ExtensionScannerInput(version, commit, locale, devMode, getExtraDevSystemExtensionsRoot(), true, false, translations); + const input = new ExtensionScannerInput(version, date, commit, locale, devMode, getExtraDevSystemExtensionsRoot(), true, false, translations); const extraBuiltinExtensions = Promise.all([builtInExtensions, controlFile]) .then(([builtInExtensions, control]) => new ExtraBuiltInExtensionResolver(builtInExtensions, control)) .then(resolver => ExtensionScanner.scanExtensions(input, log, resolver)); @@ -279,7 +280,7 @@ export class CachedExtensionScanner { notificationService, environmentService, USER_MANIFEST_CACHE_FILE, - new ExtensionScannerInput(version, commit, locale, devMode, environmentService.extensionsPath, false, false, translations), + new ExtensionScannerInput(version, date, commit, locale, devMode, environmentService.extensionsPath, false, false, translations), log )); @@ -288,7 +289,7 @@ export class CachedExtensionScanner { if (environmentService.isExtensionDevelopment && environmentService.extensionDevelopmentLocationURI) { const extDescsP = environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file).map(extLoc => { return ExtensionScanner.scanOneOrMultipleExtensions( - new ExtensionScannerInput(version, commit, locale, devMode, originalFSPath(extLoc), false, true, translations), log + new ExtensionScannerInput(version, date, commit, locale, devMode, originalFSPath(extLoc), false, true, translations), log ); }); developedExtensions = Promise.all(extDescsP).then((extDescArrays: IExtensionDescription[][]) => { diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index 089431f6af..d73a04eb87 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -15,7 +15,7 @@ import { IWorkbenchExtensionEnablementService, EnablementState, IWebExtensionsSc import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IRemoteExtensionHostDataProvider, RemoteExtensionHost, IRemoteExtensionHostInitData } from 'vs/workbench/services/extensions/common/remoteExtensionHost'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError, RemoteTrustOption, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; @@ -42,12 +42,8 @@ import { Schemas } from 'vs/base/common/network'; import { ExtensionHostExitCode } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { updateProxyConfigurationsScope } from 'vs/platform/request/common/request'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { Codicon } from 'vs/base/common/codicons'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; - -const MACHINE_PROMPT = false; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; export class ExtensionService extends AbstractExtensionService implements IExtensionService { @@ -75,7 +71,6 @@ export class ExtensionService extends AbstractExtensionService implements IExten @IRemoteExplorerService private readonly _remoteExplorerService: IRemoteExplorerService, @IExtensionGalleryService private readonly _extensionGalleryService: IExtensionGalleryService, @ILogService private readonly _logService: ILogService, - @IDialogService private readonly _dialogService: IDialogService, @IWorkspaceTrustManagementService private readonly _workspaceTrustManagementService: IWorkspaceTrustManagementService, @IExtensionManifestPropertiesService extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { @@ -144,7 +139,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten return { getInitData: async () => { if (isInitialStart) { - const localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions()); + // Here we load even extensions that would be disabled by workspace trust + const localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions(), /* ignore workspace trust */true); const runningLocation = this._runningLocationClassifier.determineRunningLocation(localExtensions, []); const localProcessExtensions = filterByRunningLocation(localExtensions, runningLocation, desiredRunningLocation); return { @@ -343,11 +339,24 @@ export class ExtensionService extends AbstractExtensionService implements IExten const remoteAuthority = this._environmentService.remoteAuthority; const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess)!; - const localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions()); let remoteEnv: IRemoteAgentEnvironment | null = null; let remoteExtensions: IExtensionDescription[] = []; if (remoteAuthority) { + + this._remoteAuthorityResolverService._setCanonicalURIProvider(async (uri) => { + if (uri.scheme !== Schemas.vscodeRemote || uri.authority !== remoteAuthority) { + // The current remote authority resolver cannot give the canonical URI for this URI + return uri; + } + const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess)!; + return localProcessExtensionHost.getCanonicalURI(remoteAuthority, uri); + }); + + // Now that the canonical URI provider has been registered, we need to wait for the trust state to be + // calculated. The trust state will be used while resolving the authority, however the resolver can + // override the trust state through the resolver result. + await this._workspaceTrustManagementService.workspaceResolved; let resolverResult: ResolverResult; try { @@ -364,42 +373,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._remoteAuthorityResolverService._setResolvedAuthorityError(remoteAuthority, err); // Proceed with the local extension host - await this._startLocalExtensionHost(localExtensions); + await this._startLocalExtensionHost(); return; } - let promptForMachineTrust = MACHINE_PROMPT; - - if (resolverResult.options?.trust === RemoteTrustOption.DisableTrust) { - promptForMachineTrust = false; - this._workspaceTrustManagementService.setWorkspaceTrust(true); - } else if (resolverResult.options?.trust === RemoteTrustOption.MachineTrusted) { - promptForMachineTrust = false; - } - - if (promptForMachineTrust) { - const dialogResult = await this._dialogService.show( - Severity.Info, - nls.localize('machineTrustQuestion', "Do you trust the machine you're connecting to?"), - [nls.localize('yes', "Yes, connect."), nls.localize('no', "No, do not connect.")], - { - cancelId: 1, - custom: { - icon: Codicon.remoteExplorer - }, - // checkbox: { label: nls.localize('remember', "Remember my choice"), checked: true } - } - ); - - if (dialogResult.choice !== 0) { - // Did not confirm trust - this._notificationService.notify({ severity: Severity.Warning, message: nls.localize('trustFailure', "Refused to connect to untrusted machine.") }); - // Proceed with the local extension host - await this._startLocalExtensionHost(localExtensions); - return; - } - } - // set the resolved authority this._remoteAuthorityResolverService._setResolvedAuthority(resolverResult.authority, resolverResult.options); this._remoteExplorerService.setTunnelInformation(resolverResult.tunnelInformation); @@ -420,23 +397,31 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._remoteAgentService.getEnvironment(), this._remoteAgentService.scanExtensions() ]); - remoteExtensions = this._checkEnabledAndProposedAPI(remoteExtensions); if (!remoteEnv) { this._notificationService.notify({ severity: Severity.Error, message: nls.localize('getEnvironmentFailure', "Could not fetch remote environment") }); // Proceed with the local extension host - await this._startLocalExtensionHost(localExtensions); + await this._startLocalExtensionHost(); return; } updateProxyConfigurationsScope(remoteEnv.useHostProxy ? ConfigurationScope.APPLICATION : ConfigurationScope.MACHINE); + } else { + + this._remoteAuthorityResolverService._setCanonicalURIProvider(async (uri) => uri); + } - await this._startLocalExtensionHost(localExtensions, remoteAuthority, remoteEnv, remoteExtensions); + await this._startLocalExtensionHost(remoteAuthority, remoteEnv, remoteExtensions); } - private async _startLocalExtensionHost(localExtensions: IExtensionDescription[], remoteAuthority: string | undefined = undefined, remoteEnv: IRemoteAgentEnvironment | null = null, remoteExtensions: IExtensionDescription[] = []): Promise { + private async _startLocalExtensionHost(remoteAuthority: string | undefined = undefined, remoteEnv: IRemoteAgentEnvironment | null = null, remoteExtensions: IExtensionDescription[] = []): Promise { + // Ensure that the workspace trust state has been fully initialized so + // that the extension host can start with the correct set of extensions. + await this._workspaceTrustManagementService.workspaceTrustInitialized; + remoteExtensions = this._checkEnabledAndProposedAPI(remoteExtensions, false); + const localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions(), false); this._runningLocation = this._runningLocationClassifier.determineRunningLocation(localExtensions, remoteExtensions); // remove non-UI extensions from the local extensions @@ -516,7 +501,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten const allExtensions = await this._scanAllLocalExtensions(); const extension = allExtensions.filter(e => e.identifier.value === resolverExtensionId)[0]; if (extension) { - if (!this._isEnabled(extension)) { + if (!this._isEnabled(extension, false)) { const message = nls.localize('enableResolver', "Extension '{0}' is required to open the remote window.\nOK to enable?", recommendation.friendlyName); this._notificationService.prompt(Severity.Info, message, [{ diff --git a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts index af19ec1b60..b366a1c40b 100644 --- a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts @@ -180,9 +180,9 @@ export class LocalProcessExtensionHost implements IExtensionHost { } if (this._isExtensionDevHost) { - // Unset `VSCODE_NODE_CACHED_DATA_DIR` when developing extensions because it might + // Unset `VSCODE_CODE_CACHE_PATH` when developing extensions because it might // be that dependencies, that otherwise would be cached, get modified. - delete env['VSCODE_NODE_CACHED_DATA_DIR']; + delete env['VSCODE_CODE_CACHE_PATH']; } const opts = { @@ -477,8 +477,6 @@ export class LocalProcessExtensionHost implements IExtensionHost { extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI, globalStorageHome: this._environmentService.globalStorageHome, workspaceStorageHome: this._environmentService.workspaceStorageHome, - webviewResourceRoot: this._environmentService.webviewResourceRoot, - webviewCspSource: this._environmentService.webviewCspSource, }, workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : { configuration: withNullAsUndefined(workspace.configuration), @@ -628,6 +626,8 @@ export class LocalProcessExtensionHost implements IExtensionHost { // (graceful termination) protocol.send(createMessageOfType(MessageType.Terminate)); + protocol.getSocket().dispose(); + protocol.dispose(); // Give the extension host 10s, after which we will diff --git a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts index 331d75239e..99438ac249 100644 --- a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts +++ b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts @@ -18,7 +18,7 @@ import { MessageType, createMessageOfType, isMessageOfType, IExtHostSocketMessag import { ExtensionHostMain, IExitFn } from 'vs/workbench/services/extensions/common/extensionHostMain'; import { VSBuffer } from 'vs/base/common/buffer'; import { IURITransformer, URITransformer, IRawURITransformer } from 'vs/base/common/uriIpc'; -import { exists } from 'vs/base/node/pfs'; +import { Promises } from 'vs/base/node/pfs'; import { realpath } from 'vs/base/node/extpath'; import { IHostUtils } from 'vs/workbench/api/common/extHostExtensionService'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -173,6 +173,9 @@ function _createExtHostProtocol(): Promise { }); socket.once('error', reject); + socket.on('close', () => { + onTerminate('renderer closed the socket'); + }); }); } } @@ -323,7 +326,7 @@ export async function startExtensionHostProcess(): Promise { const hostUtils = new class NodeHost implements IHostUtils { declare readonly _serviceBrand: undefined; exit(code: number) { nativeExit(code); } - exists(path: string) { return exists(path); } + exists(path: string) { return Promises.exists(path); } realpath(path: string) { return realpath(path); } }; diff --git a/src/vs/workbench/services/extensions/node/extensionPoints.ts b/src/vs/workbench/services/extensions/node/extensionPoints.ts index 432ea1b6fd..a8326566f8 100644 --- a/src/vs/workbench/services/extensions/node/extensionPoints.ts +++ b/src/vs/workbench/services/extensions/node/extensionPoints.ts @@ -3,7 +3,6 @@ * 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 semver from 'vs/base/common/semver/semver'; @@ -30,14 +29,16 @@ export interface NlsConfiguration { abstract class ExtensionManifestHandler { protected readonly _ourVersion: string; + protected readonly _ourProductDate: string | undefined; protected readonly _log: ILog; protected readonly _absoluteFolderPath: string; protected readonly _isBuiltin: boolean; protected readonly _isUnderDevelopment: boolean; protected readonly _absoluteManifestPath: string; - constructor(ourVersion: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean) { + constructor(ourVersion: string, ourProductDate: string | undefined, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean) { this._ourVersion = ourVersion; + this._ourProductDate = ourProductDate; this._log = log; this._absoluteFolderPath = absoluteFolderPath; this._isBuiltin = isBuiltin; @@ -58,7 +59,7 @@ class ExtensionManifestParser extends ExtensionManifestHandler { } public parse(): Promise { - return fs.promises.readFile(this._absoluteManifestPath).then((manifestContents) => { + return pfs.Promises.readFile(this._absoluteManifestPath).then((manifestContents) => { const errors: json.ParseError[] = []; const manifest = ExtensionManifestParser._fastParseJSON(manifestContents.toString(), errors); if (json.getNodeType(manifest) !== 'object') { @@ -94,8 +95,8 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { private readonly _nlsConfig: NlsConfiguration; - constructor(ourVersion: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration) { - super(ourVersion, log, absoluteFolderPath, isBuiltin, isUnderDevelopment); + constructor(ourVersion: string, ourProductDate: string | undefined, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration) { + super(ourVersion, ourProductDate, log, absoluteFolderPath, isBuiltin, isUnderDevelopment); this._nlsConfig = nlsConfig; } @@ -131,7 +132,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { let translationPath = this._nlsConfig.translations[translationId]; let localizedMessages: Promise; if (translationPath) { - localizedMessages = fs.promises.readFile(translationPath, 'utf8').then((content) => { + localizedMessages = pfs.Promises.readFile(translationPath, 'utf8').then((content) => { let errors: json.ParseError[] = []; let translationBundle: TranslationBundle = json.parse(content, errors); if (errors.length > 0) { @@ -156,7 +157,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { if (!messageBundle.localized) { return { values: undefined, default: messageBundle.original }; } - return fs.promises.readFile(messageBundle.localized, 'utf8').then(messageBundleContent => { + return pfs.Promises.readFile(messageBundle.localized, 'utf8').then(messageBundleContent => { let errors: json.ParseError[] = []; let messages: MessageBag = json.parse(messageBundleContent, errors); if (errors.length > 0) { @@ -205,7 +206,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { private static resolveOriginalMessageBundle(originalMessageBundle: string | null, errors: json.ParseError[]) { return new Promise<{ [key: string]: string; } | null>((c, e) => { if (originalMessageBundle) { - fs.promises.readFile(originalMessageBundle).then(originalBundleContent => { + pfs.Promises.readFile(originalMessageBundle).then(originalBundleContent => { c(json.parse(originalBundleContent.toString(), errors)); }, (err) => { c(null); @@ -321,7 +322,7 @@ class ExtensionManifestValidator extends ExtensionManifestHandler { extensionDescription.isUnderDevelopment = this._isUnderDevelopment; let notices: string[] = []; - if (!ExtensionManifestValidator.isValidExtensionDescription(this._ourVersion, this._absoluteFolderPath, extensionDescription, notices)) { + if (!ExtensionManifestValidator.isValidExtensionDescription(this._ourVersion, this._ourProductDate, this._absoluteFolderPath, extensionDescription, notices)) { notices.forEach((error) => { this._log.error(this._absoluteFolderPath, error); }); @@ -347,7 +348,7 @@ class ExtensionManifestValidator extends ExtensionManifestHandler { return extensionDescription; } - private static isValidExtensionDescription(version: string, extensionFolderPath: string, extensionDescription: IExtensionDescription, notices: string[]): boolean { + private static isValidExtensionDescription(version: string, productDate: string | undefined, extensionFolderPath: string, extensionDescription: IExtensionDescription, notices: string[]): boolean { if (!ExtensionManifestValidator.baseIsValidExtensionDescription(extensionFolderPath, extensionDescription, notices)) { return false; @@ -358,7 +359,7 @@ class ExtensionManifestValidator extends ExtensionManifestHandler { return false; } - return isValidExtensionVersion(version, extensionDescription, notices); + return isValidExtensionVersion(version, productDate, extensionDescription, notices); } private static baseIsValidExtensionDescription(extensionFolderPath: string, extensionDescription: IExtensionDescription, notices: string[]): boolean { @@ -456,6 +457,7 @@ export class ExtensionScannerInput { constructor( public readonly ourVersion: string, + public readonly ourProductDate: string | undefined, public readonly commit: string | undefined, public readonly locale: string | undefined, public readonly devMode: boolean, @@ -479,6 +481,7 @@ export class ExtensionScannerInput { public static equals(a: ExtensionScannerInput, b: ExtensionScannerInput): boolean { return ( a.ourVersion === b.ourVersion + && a.ourProductDate === b.ourProductDate && a.commit === b.commit && a.locale === b.locale && a.devMode === b.devMode @@ -505,7 +508,7 @@ class DefaultExtensionResolver implements IExtensionResolver { constructor(private root: string) { } resolveExtensions(): Promise { - return pfs.readDirsInDir(this.root) + return pfs.Promises.readDirsInDir(this.root) .then(folders => folders.map(name => ({ name, path: path.join(this.root, name) }))); } } @@ -515,23 +518,23 @@ export class ExtensionScanner { /** * Read the extension defined in `absoluteFolderPath` */ - private static scanExtension(version: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration): Promise { + private static scanExtension(version: string, productDate: string | undefined, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration): Promise { absoluteFolderPath = path.normalize(absoluteFolderPath); - let parser = new ExtensionManifestParser(version, log, absoluteFolderPath, isBuiltin, isUnderDevelopment); + let parser = new ExtensionManifestParser(version, productDate, log, absoluteFolderPath, isBuiltin, isUnderDevelopment); return parser.parse().then((extensionDescription) => { if (extensionDescription === null) { return null; } - let nlsReplacer = new ExtensionManifestNLSReplacer(version, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig); + let nlsReplacer = new ExtensionManifestNLSReplacer(version, productDate, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig); return nlsReplacer.replaceNLS(extensionDescription); }).then((extensionDescription) => { if (extensionDescription === null) { return null; } - let validator = new ExtensionManifestValidator(version, log, absoluteFolderPath, isBuiltin, isUnderDevelopment); + let validator = new ExtensionManifestValidator(version, productDate, log, absoluteFolderPath, isBuiltin, isUnderDevelopment); return validator.validate(extensionDescription); }); } @@ -552,7 +555,7 @@ export class ExtensionScanner { let obsolete: { [folderName: string]: boolean; } = {}; if (!isBuiltin) { try { - const obsoleteFileContents = await fs.promises.readFile(path.join(absoluteFolderPath, '.obsolete'), 'utf8'); + const obsoleteFileContents = await pfs.Promises.readFile(path.join(absoluteFolderPath, '.obsolete'), 'utf8'); obsolete = JSON.parse(obsoleteFileContents); } catch (err) { // Don't care @@ -569,7 +572,7 @@ export class ExtensionScanner { } const nlsConfig = ExtensionScannerInput.createNLSConfig(input); - let _extensionDescriptions = await Promise.all(refs.map(r => this.scanExtension(input.ourVersion, log, r.path, isBuiltin, isUnderDevelopment, nlsConfig))); + let _extensionDescriptions = await Promise.all(refs.map(r => this.scanExtension(input.ourVersion, input.ourProductDate, log, r.path, isBuiltin, isUnderDevelopment, nlsConfig))); let extensionDescriptions = arrays.coalesce(_extensionDescriptions); extensionDescriptions = extensionDescriptions.filter(item => item !== null && !obsolete[new ExtensionIdentifierWithVersion({ id: getGalleryExtensionId(item.publisher, item.name) }, item.version).key()]); @@ -604,7 +607,7 @@ export class ExtensionScanner { return pfs.SymlinkSupport.existsFile(path.join(absoluteFolderPath, MANIFEST_FILE)).then((exists) => { if (exists) { const nlsConfig = ExtensionScannerInput.createNLSConfig(input); - return this.scanExtension(input.ourVersion, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig).then((extensionDescription) => { + return this.scanExtension(input.ourVersion, input.ourProductDate, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig).then((extensionDescription) => { if (extensionDescription === null) { return []; } @@ -623,7 +626,7 @@ export class ExtensionScanner { const isBuiltin = input.isBuiltin; const isUnderDevelopment = input.isUnderDevelopment; const nlsConfig = ExtensionScannerInput.createNLSConfig(input); - return this.scanExtension(input.ourVersion, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig); + return this.scanExtension(input.ourVersion, input.ourProductDate, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig); } public static mergeBuiltinExtensions(builtinExtensions: Promise, extraBuiltinExtensions: Promise): Promise { diff --git a/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts b/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts index 7cb4697b3c..a0bdddedbf 100644 --- a/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts +++ b/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts @@ -12,11 +12,13 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IProductService } from 'vs/platform/product/common/productService'; import { isWeb } from 'vs/base/common/platform'; +import { TestWorkspaceTrustManagementService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; suite('ExtensionManifestPropertiesService - ExtensionKind', () => { function check(manifest: Partial, expected: ExtensionKind[]): void { - const extensionManifestPropertiesService = new ExtensionManifestPropertiesService(TestProductService, new TestConfigurationService()); + const extensionManifestPropertiesService = new ExtensionManifestPropertiesService(TestProductService, new TestConfigurationService(), new TestWorkspaceTrustManagementService()); assert.deepStrictEqual(extensionManifestPropertiesService.deduceExtensionKind(manifest), expected); } @@ -80,6 +82,7 @@ if (!isWeb) { test('test extension workspace trust request when main entry point is missing', () => { instantiationService.stub(IProductService, >{}); + instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); const extensionMaifest = getExtensionManifest(); assertUntrustedWorkspaceSupport(extensionMaifest, true); @@ -87,7 +90,7 @@ if (!isWeb) { test('test extension workspace trust request when workspace trust is disabled', async () => { instantiationService.stub(IProductService, >{}); - await testConfigurationService.setUserConfiguration('security', { workspace: { trust: { enabled: false } } }); + instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService(false)); const extensionMaifest = getExtensionManifest({ main: './out/extension.js' }); assertUntrustedWorkspaceSupport(extensionMaifest, true); @@ -95,37 +98,34 @@ if (!isWeb) { test('test extension workspace trust request when override exists in settings.json', async () => { instantiationService.stub(IProductService, >{}); + instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); - await testConfigurationService.setUserConfiguration('security', { workspace: { trust: { extensionUntrustedSupport: { 'pub.a': { supported: true } } } } }); + await testConfigurationService.setUserConfiguration('extensions', { supportUntrustedWorkspaces: { 'pub.a': { supported: true } } }); const extensionMaifest = getExtensionManifest({ main: './out/extension.js', capabilities: { untrustedWorkspaces: { supported: 'limited' } } }); assertUntrustedWorkspaceSupport(extensionMaifest, true); }); test('test extension workspace trust request when override for the version exists in settings.json', async () => { instantiationService.stub(IProductService, >{}); + instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); - await testConfigurationService.setUserConfiguration('security', { workspace: { trust: { extensionUntrustedSupport: { 'pub.a': { supported: true, version: '1.0.0' } } } } }); + await testConfigurationService.setUserConfiguration('extensions', { supportUntrustedWorkspaces: { 'pub.a': { supported: true, version: '1.0.0' } } }); const extensionMaifest = getExtensionManifest({ main: './out/extension.js', capabilities: { untrustedWorkspaces: { supported: 'limited' } } }); assertUntrustedWorkspaceSupport(extensionMaifest, true); }); test('test extension workspace trust request when override for a different version exists in settings.json', async () => { instantiationService.stub(IProductService, >{}); + instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); - await testConfigurationService.setUserConfiguration('security', { - workspace: { - trust: { - enabled: true, - extensionUntrustedSupport: { 'pub.a': { supported: true, version: '2.0.0' } } - } - } - }); + await testConfigurationService.setUserConfiguration('extensions', { supportUntrustedWorkspaces: { 'pub.a': { supported: true, version: '2.0.0' } } }); const extensionMaifest = getExtensionManifest({ main: './out/extension.js', capabilities: { untrustedWorkspaces: { supported: 'limited' } } }); assertUntrustedWorkspaceSupport(extensionMaifest, 'limited'); }); test('test extension workspace trust request when default exists in product.json', () => { instantiationService.stub(IProductService, >{ extensionUntrustedWorkspaceSupport: { 'pub.a': { default: true } } }); + instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); const extensionMaifest = getExtensionManifest({ main: './out/extension.js' }); assertUntrustedWorkspaceSupport(extensionMaifest, true); @@ -133,6 +133,7 @@ if (!isWeb) { test('test extension workspace trust request when override exists in product.json', () => { instantiationService.stub(IProductService, >{ extensionUntrustedWorkspaceSupport: { 'pub.a': { override: 'limited' } } }); + instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); const extensionMaifest = getExtensionManifest({ main: './out/extension.js', capabilities: { untrustedWorkspaces: { supported: true } } }); assertUntrustedWorkspaceSupport(extensionMaifest, 'limited'); @@ -140,6 +141,7 @@ if (!isWeb) { test('test extension workspace trust request when value exists in package.json', () => { instantiationService.stub(IProductService, >{}); + instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); const extensionMaifest = getExtensionManifest({ main: './out/extension.js', capabilities: { untrustedWorkspaces: { supported: 'limited' } } }); assertUntrustedWorkspaceSupport(extensionMaifest, 'limited'); @@ -147,6 +149,7 @@ if (!isWeb) { test('test extension workspace trust request when no value exists in package.json', () => { instantiationService.stub(IProductService, >{}); + instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); const extensionMaifest = getExtensionManifest({ main: './out/extension.js' }); assertUntrustedWorkspaceSupport(extensionMaifest, false); diff --git a/src/vs/workbench/services/files/browser/elevatedFileService.ts b/src/vs/workbench/services/files/browser/elevatedFileService.ts index 2aa9dce65a..4801b0cc3a 100644 --- a/src/vs/workbench/services/files/browser/elevatedFileService.ts +++ b/src/vs/workbench/services/files/browser/elevatedFileService.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; diff --git a/src/vs/workbench/services/files/common/elevatedFileService.ts b/src/vs/workbench/services/files/common/elevatedFileService.ts index c607b9bebb..0c06e24ea8 100644 --- a/src/vs/workbench/services/files/common/elevatedFileService.ts +++ b/src/vs/workbench/services/files/common/elevatedFileService.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/workbench/services/files/electron-sandbox/elevatedFileService.ts b/src/vs/workbench/services/files/electron-sandbox/elevatedFileService.ts index e47aa8eae0..95dde8bc17 100644 --- a/src/vs/workbench/services/files/electron-sandbox/elevatedFileService.ts +++ b/src/vs/workbench/services/files/electron-sandbox/elevatedFileService.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; diff --git a/src/vs/workbench/services/history/browser/history.ts b/src/vs/workbench/services/history/browser/history.ts index 3f809b664d..63b255d892 100644 --- a/src/vs/workbench/services/history/browser/history.ts +++ b/src/vs/workbench/services/history/browser/history.ts @@ -7,7 +7,8 @@ import { localize } from 'vs/nls'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IEditor } from 'vs/editor/common/editorCommon'; import { ITextEditorOptions, IResourceEditorInput, TextEditorSelectionRevealType, IEditorOptions } from 'vs/platform/editor/common/editor'; -import { IEditorInput, IEditorPane, EditorExtensions, EditorInput, IEditorCloseEvent, IEditorInputFactoryRegistry, EditorResourceAccessor, IEditorIdentifier, GroupIdentifier, EditorsOrder, SideBySideEditor } from 'vs/workbench/common/editor'; +import { IEditorInput, IEditorPane, EditorExtensions, IEditorCloseEvent, IEditorInputFactoryRegistry, EditorResourceAccessor, IEditorIdentifier, GroupIdentifier, EditorsOrder, SideBySideEditor } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { FileChangesEvent, IFileService, FileChangeType, FILES_EXCLUDE_CONFIG, FileOperationEvent, FileOperation } from 'vs/platform/files/common/files'; @@ -398,7 +399,7 @@ export class HistoryService extends Disposable implements IHistoryService { return this.editorService.openEditor(location.input, options); } - return this.editorService.openEditor({ resource: (location.input as IResourceEditorInput).resource, options }); + return this.editorService.openEditor({ resource: location.input.resource, options }); } private handleEditorEventInNavigationStack(control: IEditorPane | undefined, event?: ICursorPositionChangedEvent): void { @@ -1011,7 +1012,7 @@ export class HistoryService extends Disposable implements IHistoryService { const editorSerializer = this.editorInputFactory.getEditorInputSerializer(editorInputJSON.typeId); if (editorSerializer) { const input = editorSerializer.deserialize(this.instantiationService, editorInputJSON.deserialized); - if (input) { + if (input instanceof EditorInput) { this.onEditorDispose(input, () => this.removeFromHistory(input), this.editorHistoryListeners); } diff --git a/src/vs/workbench/services/history/test/browser/history.test.ts b/src/vs/workbench/services/history/test/browser/history.test.ts index dce0fb9b4c..009c729ed7 100644 --- a/src/vs/workbench/services/history/test/browser/history.test.ts +++ b/src/vs/workbench/services/history/test/browser/history.test.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { EditorOptions } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; import { workbenchInstantiationService, TestFileEditorInput, registerTestEditor, createEditorPart } from 'vs/workbench/test/browser/workbenchTestServices'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; @@ -18,7 +17,7 @@ import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { timeout } from 'vs/base/common/async'; import { Event } from 'vs/base/common/event'; -suite.skip('HistoryService', function () { +suite.skip('HistoryService', function () { // {{SQL CARBON EDIT}} Skip suite const TEST_EDITOR_ID = 'MyTestEditorForEditorHistory'; const TEST_EDITOR_INPUT_ID = 'testEditorInputForHistoyService'; @@ -53,11 +52,11 @@ suite.skip('HistoryService', function () { const [part, historyService, editorService] = await createServices(); const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); - await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input1, { pinned: true }); assert.strictEqual(part.activeGroup.activeEditor, input1); const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); - await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input2, { pinned: true }); assert.strictEqual(part.activeGroup.activeEditor, input2); let editorChangePromise = Event.toPromise(editorService.onDidActiveEditorChange); @@ -78,10 +77,10 @@ suite.skip('HistoryService', function () { assert.strictEqual(history.length, 0); const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); - await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input1, { pinned: true }); const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); - await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input2, { pinned: true }); history = historyService.getHistory(); assert.strictEqual(history.length, 2); @@ -98,7 +97,7 @@ suite.skip('HistoryService', function () { assert.ok(!historyService.getLastActiveFile('foo')); const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); - await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input1, { pinned: true }); assert.strictEqual(historyService.getLastActiveFile('foo')?.toString(), input1.resource.toString()); }); @@ -109,10 +108,10 @@ suite.skip('HistoryService', function () { const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); - await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input1, { pinned: true }); assert.strictEqual(part.activeGroup.activeEditor, input1); - await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input2, { pinned: true }); assert.strictEqual(part.activeGroup.activeEditor, input2); let editorChangePromise = Event.toPromise(editorService.onDidActiveEditorChange); @@ -145,8 +144,8 @@ suite.skip('HistoryService', function () { const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - await sideGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input1, { pinned: true }); + await sideGroup.openEditor(input2, { pinned: true }); let editorChangePromise = Event.toPromise(editorService.onDidActiveEditorChange); historyService.openPreviouslyUsedEditor(); @@ -169,9 +168,9 @@ suite.skip('HistoryService', function () { const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID); const input4 = new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID); - await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); - await part.activeGroup.openEditor(input3, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input1, { pinned: true }); + await part.activeGroup.openEditor(input2, { pinned: true }); + await part.activeGroup.openEditor(input3, { pinned: true }); let editorChangePromise = Event.toPromise(editorService.onDidActiveEditorChange); historyService.openPreviouslyUsedEditor(); @@ -179,7 +178,7 @@ suite.skip('HistoryService', function () { assert.strictEqual(part.activeGroup.activeEditor, input2); await timeout(0); - await part.activeGroup.openEditor(input4, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input4, { pinned: true }); editorChangePromise = Event.toPromise(editorService.onDidActiveEditorChange); historyService.openPreviouslyUsedEditor(); diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index 1cf87d85bb..add90b8277 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -11,7 +11,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWindowSettings, IWindowOpenable, IOpenWindowOptions, isFolderToOpen, isWorkspaceToOpen, isFileToOpen, IOpenEmptyWindowOptions, IPathData, IFileToOpen } from 'vs/platform/windows/common/windows'; import { pathsToEditors } from 'vs/workbench/common/editor'; -import { whenTextEditorClosed } from 'vs/workbench/browser/editor'; +import { whenEditorClosed } from 'vs/workbench/browser/editor'; import { IFileService } from 'vs/platform/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; import { IModifierKeyStatus, ModifierKeyEmitter, trackFocus } from 'vs/base/browser/dom'; @@ -256,8 +256,8 @@ export class BrowserHostService extends Disposable implements IHostService { // Same Window: open via editor service in current window if (this.shouldReuse(options, true /* file */)) { editorService.openEditor({ - leftResource: editors[0].resource, - rightResource: editors[1].resource, + originalInput: { resource: editors[0].resource }, + modifiedInput: { resource: editors[1].resource }, options: { pinned: true } }); } @@ -292,7 +292,7 @@ export class BrowserHostService extends Disposable implements IHostService { openables = [openable]; } - editorService.openEditors(await pathsToEditors(openables, this.fileService)); + editorService.openEditors(await pathsToEditors(openables, this.fileService), undefined, { validateTrust: true }); } // New Window: open into empty window @@ -315,7 +315,7 @@ export class BrowserHostService extends Disposable implements IHostService { (async () => { // Wait for the resources to be closed in the text editor... - await this.instantiationService.invokeFunction(accessor => whenTextEditorClosed(accessor, fileOpenables.map(fileOpenable => fileOpenable.fileUri))); + await this.instantiationService.invokeFunction(accessor => whenEditorClosed(accessor, fileOpenables.map(fileOpenable => fileOpenable.fileUri))); // ...before deleting the wait marker file await this.fileService.del(waitMarkerFileURI); diff --git a/src/vs/workbench/services/host/browser/host.ts b/src/vs/workbench/services/host/browser/host.ts index 2cd708e9bc..4c15cd5066 100644 --- a/src/vs/workbench/services/host/browser/host.ts +++ b/src/vs/workbench/services/host/browser/host.ts @@ -12,7 +12,7 @@ export const IHostService = createDecorator('hostService'); /** * A set of methods supported in both web and native environments. * - * @see `INativeHostService` for methods that are specific to native + * @see {@link INativeHostService} for methods that are specific to native * environments. */ export interface IHostService { diff --git a/src/vs/workbench/services/hover/browser/hoverService.ts b/src/vs/workbench/services/hover/browser/hoverService.ts index 4119ba4add..3e49cb2923 100644 --- a/src/vs/workbench/services/hover/browser/hoverService.ts +++ b/src/vs/workbench/services/hover/browser/hoverService.ts @@ -8,11 +8,12 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { editorHoverBackground, editorHoverBorder, textLinkForeground, editorHoverForeground, editorHoverStatusBarBackground, textCodeBlockBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { IHoverService, IHoverOptions } from 'vs/workbench/services/hover/browser/hover'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { HoverWidget } from 'vs/workbench/services/hover/browser/hoverWidget'; import { IContextViewProvider, IDelegate } from 'vs/base/browser/ui/contextview/contextview'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { addDisposableListener, EventType } from 'vs/base/browser/dom'; export class HoverService implements IHoverService { declare readonly _serviceBrand: undefined; @@ -21,8 +22,10 @@ export class HoverService implements IHoverService { constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IContextViewService private readonly _contextViewService: IContextViewService + @IContextViewService private readonly _contextViewService: IContextViewService, + @IContextMenuService contextMenuService: IContextMenuService ) { + contextMenuService.onDidShowContextMenu(() => this.hideHover()); } showHover(options: IHoverOptions, focus?: boolean): IDisposable | undefined { @@ -31,17 +34,28 @@ export class HoverService implements IHoverService { } this._currentHoverOptions = options; + const hoverDisposables = new DisposableStore(); const hover = this._instantiationService.createInstance(HoverWidget, options); - hover.onDispose(() => this._currentHoverOptions = undefined); + hover.onDispose(() => { + this._currentHoverOptions = undefined; + hoverDisposables.dispose(); + }); const provider = this._contextViewService as IContextViewProvider; provider.showContextView(new HoverContextViewDelegate(hover, focus)); hover.onRequestLayout(() => provider.layout()); + if ('targetElements' in options.target) { + for (const element of options.target.targetElements) { + hoverDisposables.add(addDisposableListener(element, EventType.CLICK, () => this.hideHover())); + } + } else { + hoverDisposables.add(addDisposableListener(options.target, EventType.CLICK, () => this.hideHover())); + } if ('IntersectionObserver' in window) { const observer = new IntersectionObserver(e => this._intersectionChange(e, hover), { threshold: 0 }); const firstTargetElement = 'targetElements' in options.target ? options.target.targetElements[0] : options.target; observer.observe(firstTargetElement); - hover.onDispose(() => observer.disconnect()); + hoverDisposables.add(toDisposable(() => observer.disconnect())); } return hover; diff --git a/src/vs/workbench/services/hover/browser/hoverWidget.ts b/src/vs/workbench/services/hover/browser/hoverWidget.ts index b08f5dbc96..cd2d5b91d4 100644 --- a/src/vs/workbench/services/hover/browser/hoverWidget.ts +++ b/src/vs/workbench/services/hover/browser/hoverWidget.ts @@ -18,6 +18,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; +import { isString } from 'vs/base/common/types'; const $ = dom.$; type TargetRect = { @@ -66,7 +67,7 @@ export class HoverWidget extends Widget { ) { super(); - this._linkHandler = options.linkHandler || this._openerService.open; + this._linkHandler = options.linkHandler || (url => this._openerService.open(url, { allowCommands: (!isString(options.text) && options.text.isTrusted) })); this._target = 'targetElements' in options.target ? options.target : new ElementHoverTarget(options.target); diff --git a/src/vs/workbench/services/issue/electron-sandbox/issueService.ts b/src/vs/workbench/services/issue/electron-sandbox/issueService.ts index a687d9f98c..aa3ce1da21 100644 --- a/src/vs/workbench/services/issue/electron-sandbox/issueService.ts +++ b/src/vs/workbench/services/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, 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, listActiveSelectionBackground, listActiveSelectionForeground, listFocusOutline, listFocusBackground, listFocusForeground, activeContrastBorder } 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'; @@ -19,6 +19,7 @@ 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'; import { registerMainProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; export class WorkbenchIssueService implements IWorkbenchIssueService { declare readonly _serviceBrand: undefined; @@ -29,6 +30,7 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IProductService private readonly productService: IProductService, @ITASExperimentService private readonly experimentService: ITASExperimentService, @IAuthenticationService private readonly authenticationService: IAuthenticationService @@ -70,15 +72,24 @@ 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')); + + let githubAccessToken = ''; + try { + const githubSessions = await this.authenticationService.getSessions('github'); + const potentialSessions = githubSessions.filter(session => session.scopes.includes('repo')); + githubAccessToken = potentialSessions[0]?.accessToken; + } catch (e) { + // Ignore + } + const theme = this.themeService.getColorTheme(); const issueReporterData: IssueReporterData = Object.assign({ styles: getIssueReporterStyles(theme), zoomLevel: getZoomLevel(), enabledExtensions: extensionData, experiments: experiments?.join('\n'), - githubAccessToken: potentialSessions[0]?.accessToken + restrictedMode: !this.workspaceTrustManagementService.isWorkpaceTrusted(), + githubAccessToken, }, dataOverrides); return this.issueService.openReporter(issueReporterData); } @@ -91,8 +102,14 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { styles: { backgroundColor: getColor(theme, editorBackground), color: getColor(theme, editorForeground), - hoverBackground: getColor(theme, listHoverBackground), - hoverForeground: getColor(theme, listHoverForeground) + listHoverBackground: getColor(theme, listHoverBackground), + listHoverForeground: getColor(theme, listHoverForeground), + listFocusBackground: getColor(theme, listFocusBackground), + listFocusForeground: getColor(theme, listFocusForeground), + listFocusOutline: getColor(theme, listFocusOutline), + listActiveSelectionBackground: getColor(theme, listActiveSelectionBackground), + listActiveSelectionForeground: getColor(theme, listActiveSelectionForeground), + listHoverOutline: getColor(theme, activeContrastBorder), }, platform: platform, applicationName: this.productService.applicationName diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/_.contribution.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/_.contribution.ts index 32e198edf6..f5c99b4e7d 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/_.contribution.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/_.contribution.ts @@ -20,4 +20,4 @@ export class KeyboardLayoutContribution { registerKeyboardLayout(layout: IKeymapInfo) { this._layoutInfos.push(layout); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/de.linux.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/de.linux.ts index 8a397dc600..655898ac79 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/de.linux.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/de.linux.ts @@ -184,4 +184,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ MailForward: [], MailSend: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/de.win.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/de.win.ts index b19368a031..ee0d9d1981 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/de.win.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/de.win.ts @@ -166,4 +166,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ BrowserRefresh: [], BrowserFavorites: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/dk.win.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/dk.win.ts index aecbd29691..39f9c73663 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/dk.win.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/dk.win.ts @@ -167,4 +167,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ BrowserFavorites: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en-ext.darwin.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en-ext.darwin.ts index 661af5fee5..91604dd516 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en-ext.darwin.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en-ext.darwin.ts @@ -129,4 +129,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ AltRight: [], MetaRight: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en-in.win.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en-in.win.ts index 2274e2ab64..781ae91501 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en-in.win.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en-in.win.ts @@ -166,4 +166,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ BrowserRefresh: [], BrowserFavorites: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en-intl.darwin.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en-intl.darwin.ts index 1bc49b2b8b..1a845ca63e 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en-intl.darwin.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en-intl.darwin.ts @@ -129,4 +129,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ AltRight: [], MetaRight: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en-uk.darwin.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en-uk.darwin.ts index 925559f424..f0235c8587 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en-uk.darwin.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en-uk.darwin.ts @@ -128,4 +128,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ AltRight: [], MetaRight: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en-uk.win.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en-uk.win.ts index 47fb8e477f..c2a0d29d4c 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en-uk.win.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en-uk.win.ts @@ -167,4 +167,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ BrowserFavorites: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en.darwin.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en.darwin.ts index 9d8ea89ac1..2034356408 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en.darwin.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en.darwin.ts @@ -137,4 +137,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ AltRight: [], MetaRight: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en.win.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en.win.ts index 02fe55f854..e5824ab035 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en.win.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en.win.ts @@ -171,4 +171,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ BrowserRefresh: [], BrowserFavorites: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/es-latin.win.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/es-latin.win.ts index 32d103802c..4b08611e73 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/es-latin.win.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/es-latin.win.ts @@ -167,4 +167,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ BrowserFavorites: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/es.darwin.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/es.darwin.ts index f672b88114..f4a5cb7a5a 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/es.darwin.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/es.darwin.ts @@ -129,4 +129,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ AltRight: [], MetaRight: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/es.linux.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/es.linux.ts index 9674c9fbd2..a635b13be4 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/es.linux.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/es.linux.ts @@ -184,4 +184,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ MailForward: [], MailSend: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/es.win.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/es.win.ts index c05628151d..db59e61790 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/es.win.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/es.win.ts @@ -166,4 +166,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ BrowserRefresh: [], BrowserFavorites: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/fr.win.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/fr.win.ts index 9b63990525..8437805e69 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/fr.win.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/fr.win.ts @@ -166,4 +166,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ BrowserRefresh: [], BrowserFavorites: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/it.darwin.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/it.darwin.ts index 00f1044217..d860298950 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/it.darwin.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/it.darwin.ts @@ -129,4 +129,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ AltRight: [], MetaRight: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/it.win.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/it.win.ts index b9526ee5da..cf16ebfc2b 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/it.win.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/it.win.ts @@ -166,4 +166,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ BrowserRefresh: [], BrowserFavorites: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/jp-roman.darwin.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/jp-roman.darwin.ts index 3b76a1aaa9..31f2750f0c 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/jp-roman.darwin.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/jp-roman.darwin.ts @@ -129,4 +129,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ AltRight: [], MetaRight: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/jp.darwin.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/jp.darwin.ts index 46798076f8..2144d21365 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/jp.darwin.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/jp.darwin.ts @@ -129,4 +129,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ AltRight: [], MetaRight: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/ko.darwin.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/ko.darwin.ts index c4aaeb7750..72a6813e94 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/ko.darwin.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/ko.darwin.ts @@ -129,4 +129,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ AltRight: [], MetaRight: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.linux.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.linux.ts index affe520976..fceac003d6 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.linux.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.linux.ts @@ -9,4 +9,4 @@ import 'vs/workbench/services/keybinding/browser/keyboardLayouts/de.linux'; import 'vs/workbench/services/keybinding/browser/keyboardLayouts/fr.linux'; import 'vs/workbench/services/keybinding/browser/keyboardLayouts/ru.linux'; -export { KeyboardLayoutContribution } from 'vs/workbench/services/keybinding/browser/keyboardLayouts/_.contribution'; \ No newline at end of file +export { KeyboardLayoutContribution } from 'vs/workbench/services/keybinding/browser/keyboardLayouts/_.contribution'; diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.win.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.win.ts index 1884cee996..e0991da788 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.win.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.win.ts @@ -26,4 +26,4 @@ import 'vs/workbench/services/keybinding/browser/keyboardLayouts/de-swiss.win'; import 'vs/workbench/services/keybinding/browser/keyboardLayouts/en-belgian.win'; import 'vs/workbench/services/keybinding/browser/keyboardLayouts/cz.win'; -export { KeyboardLayoutContribution } from 'vs/workbench/services/keybinding/browser/keyboardLayouts/_.contribution'; \ No newline at end of file +export { KeyboardLayoutContribution } from 'vs/workbench/services/keybinding/browser/keyboardLayouts/_.contribution'; diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/pl.darwin.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/pl.darwin.ts index 7aecff5814..9020530bdc 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/pl.darwin.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/pl.darwin.ts @@ -129,4 +129,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ AltRight: [], MetaRight: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/pl.win.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/pl.win.ts index 0ba8ceff01..f2f1dcebcc 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/pl.win.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/pl.win.ts @@ -166,4 +166,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ BrowserRefresh: [], BrowserFavorites: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/pt-br.win.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/pt-br.win.ts index c0f24581d5..3c848da2d0 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/pt-br.win.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/pt-br.win.ts @@ -167,4 +167,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ BrowserRefresh: [], BrowserFavorites: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/pt.win.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/pt.win.ts index 5009825331..a3bda20464 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/pt.win.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/pt.win.ts @@ -167,4 +167,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ BrowserFavorites: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/ru.darwin.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/ru.darwin.ts index 749e8c11d9..82f6c8e10c 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/ru.darwin.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/ru.darwin.ts @@ -129,4 +129,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ AltRight: [], MetaRight: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/ru.win.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/ru.win.ts index 2bf016dec7..5072a78916 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/ru.win.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/ru.win.ts @@ -166,4 +166,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ BrowserRefresh: [], BrowserFavorites: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/sv.darwin.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/sv.darwin.ts index 26c60a6f52..405d6b8d28 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/sv.darwin.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/sv.darwin.ts @@ -129,4 +129,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ AltRight: [], MetaRight: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/tr.win.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/tr.win.ts index 66a6cfbe22..b06b22ce3a 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/tr.win.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/tr.win.ts @@ -165,4 +165,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ BrowserRefresh: [], BrowserFavorites: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/zh-hans.darwin.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/zh-hans.darwin.ts index 323b77606c..62b1e0ebc3 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/zh-hans.darwin.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/zh-hans.darwin.ts @@ -128,4 +128,4 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ AltRight: [], MetaRight: [] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/browser/navigatorKeyboard.ts b/src/vs/workbench/services/keybinding/browser/navigatorKeyboard.ts index 4efb45310f..759b5030d6 100644 --- a/src/vs/workbench/services/keybinding/browser/navigatorKeyboard.ts +++ b/src/vs/workbench/services/keybinding/browser/navigatorKeyboard.ts @@ -12,4 +12,4 @@ export interface IKeyboard { } export type INavigatorWithKeyboard = Navigator & { keyboard: IKeyboard -}; \ No newline at end of file +}; diff --git a/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts b/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts index adde606428..f5e026821e 100644 --- a/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts +++ b/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts @@ -993,6 +993,7 @@ export class MacLinuxKeyboardMapper implements IKeyboardMapper { || (keyCode === KeyCode.End) || (keyCode === KeyCode.PageDown) || (keyCode === KeyCode.PageUp) + || (keyCode === KeyCode.Backspace) ) { // "Dispatch" on keyCode for these key codes to workaround issues with remote desktoping software // where the scan codes appear to be incorrect (see https://github.com/microsoft/vscode/issues/24107) diff --git a/src/vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils.ts b/src/vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils.ts index c62cb2d51e..c2874bd590 100644 --- a/src/vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils.ts +++ b/src/vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils.ts @@ -5,11 +5,10 @@ import * as assert from 'assert'; import * as path from 'vs/base/common/path'; -import { promises } from 'fs'; import { getPathFromAmdModule } from 'vs/base/test/node/testUtils'; import { Keybinding, ResolvedKeybinding, SimpleKeybinding } from 'vs/base/common/keyCodes'; import { ScanCodeBinding } from 'vs/base/common/scanCode'; -import { writeFile } from 'vs/base/node/pfs'; +import { Promises } from 'vs/base/node/pfs'; import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; import { IKeyboardMapper } from 'vs/platform/keyboardLayout/common/keyboardMapper'; @@ -53,7 +52,7 @@ export function assertResolveUserBinding(mapper: IKeyboardMapper, parts: (Simple } export function readRawMapping(file: string): Promise { - return promises.readFile(getPathFromAmdModule(require, `vs/workbench/services/keybinding/test/electron-browser/${file}.js`)).then((buff) => { + return Promises.readFile(getPathFromAmdModule(require, `vs/workbench/services/keybinding/test/electron-browser/${file}.js`)).then((buff) => { let contents = buff.toString(); let func = new Function('define', contents); let rawMappings: T | null = null; @@ -67,12 +66,12 @@ export function readRawMapping(file: string): Promise { export function assertMapping(writeFileIfDifferent: boolean, mapper: IKeyboardMapper, file: string): Promise { const filePath = path.normalize(getPathFromAmdModule(require, `vs/workbench/services/keybinding/test/electron-browser/${file}`)); - return promises.readFile(filePath).then((buff) => { + return Promises.readFile(filePath).then((buff) => { const expected = buff.toString().replace(/\r\n/g, '\n'); const actual = mapper.dumpDebugInfo().replace(/\r\n/g, '\n'); if (actual !== expected && writeFileIfDifferent) { const destPath = filePath.replace(/vscode[\/\\]out[\/\\]vs/, 'vscode/src/vs'); - writeFile(destPath, actual); + Promises.writeFile(destPath, actual); } assert.deepStrictEqual(actual, expected); }); diff --git a/src/vs/workbench/services/keybinding/test/electron-browser/linux_en_us.js b/src/vs/workbench/services/keybinding/test/electron-browser/linux_en_us.js index f1cc6af75c..2c72a65238 100644 --- a/src/vs/workbench/services/keybinding/test/electron-browser/linux_en_us.js +++ b/src/vs/workbench/services/keybinding/test/electron-browser/linux_en_us.js @@ -494,4 +494,4 @@ define({ MailForward: { value: '', withShift: '', withAltGr: '', withShiftAltGr: '' }, MailSend: { value: '', withShift: '', withAltGr: '', withShiftAltGr: '' } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/test/electron-browser/win_en_us.js b/src/vs/workbench/services/keybinding/test/electron-browser/win_en_us.js index 0d3c99fb05..75bb7246b1 100644 --- a/src/vs/workbench/services/keybinding/test/electron-browser/win_en_us.js +++ b/src/vs/workbench/services/keybinding/test/electron-browser/win_en_us.js @@ -1090,4 +1090,4 @@ define({ withAltGr: '', withShiftAltGr: '' } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/keybinding/test/electron-browser/win_por_ptb.js b/src/vs/workbench/services/keybinding/test/electron-browser/win_por_ptb.js index a617bf6d3d..d39c3a2bb7 100644 --- a/src/vs/workbench/services/keybinding/test/electron-browser/win_por_ptb.js +++ b/src/vs/workbench/services/keybinding/test/electron-browser/win_por_ptb.js @@ -1090,4 +1090,4 @@ define({ withAltGr: '', withShiftAltGr: '' } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/layout/browser/layoutService.ts b/src/vs/workbench/services/layout/browser/layoutService.ts index 989ffd73c3..39cf9ffecf 100644 --- a/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/src/vs/workbench/services/layout/browser/layoutService.ts @@ -15,6 +15,7 @@ export const IWorkbenchLayoutService = refineServiceDecorator

, R = unknown>(viewletId: string, options: IProgressCompositeOptions, promise: P) { + private showOnActivityBar

, R = unknown>(viewletId: string, options: IProgressCompositeOptions, promise: P): void { let activityProgress: IDisposable; let delayHandle: any = setTimeout(() => { delayHandle = undefined; @@ -505,8 +506,9 @@ export class ProgressService extends Disposable implements IProgressService { return promise; } - private withDialogProgress

, R = unknown>(options: IProgressOptions, task: (progress: IProgress) => P, onDidCancel?: (choice?: number) => void): P { + private withDialogProgress

, R = unknown>(options: IProgressDialogOptions, task: (progress: IProgress) => P, onDidCancel?: (choice?: number) => void): P { const disposables = new DisposableStore(); + const allowableCommands = [ 'workbench.action.quit', 'workbench.action.reloadWindow', @@ -519,7 +521,6 @@ export class ProgressService extends Disposable implements IProgressService { let dialog: Dialog; const createDialog = (message: string) => { - const buttons = options.buttons || []; buttons.push(options.cancellable ? localize('cancel', "Cancel") : localize('dismiss', "Dismiss")); @@ -529,6 +530,7 @@ export class ProgressService extends Disposable implements IProgressService { buttons, { type: 'pending', + detail: options.detail, cancelId: buttons.length - 1, keyEventProcessor: (event: StandardKeyboardEvent) => { const resolved = this.keybindingService.softDispatch(event, this.layoutService.container); @@ -544,10 +546,8 @@ export class ProgressService extends Disposable implements IProgressService { disposables.add(dialog); disposables.add(attachDialogStyler(dialog, this.themeService)); - dialog.show().then((dialogResult) => { - if (typeof onDidCancel === 'function') { - onDidCancel(dialogResult.button); - } + dialog.show().then(dialogResult => { + onDidCancel?.(dialogResult.button); dispose(dialog); }); @@ -555,11 +555,28 @@ export class ProgressService extends Disposable implements IProgressService { return dialog; }; - const updateDialog = (message?: string) => { - if (message && !dialog) { - dialog = createDialog(message); - } else if (message) { - dialog.updateMessage(message); + // In order to support the `delay` option, we use a scheduler + // that will guard each access to the dialog behind a delay + // that is either the original delay for one invocation and + // otherwise runs without delay. + let delay = options.delay ?? 0; + let latestMessage: string | undefined = undefined; + const scheduler = disposables.add(new RunOnceScheduler(() => { + delay = 0; // since we have run once, we reset the delay + + if (latestMessage && !dialog) { + dialog = createDialog(latestMessage); + } else if (latestMessage) { + dialog.updateMessage(latestMessage); + } + }, 0)); + + const updateDialog = function (message?: string): void { + latestMessage = message; + + // Make sure to only run one dialog update and not multiple + if (!scheduler.isScheduled()) { + scheduler.schedule(delay); } }; @@ -573,6 +590,10 @@ export class ProgressService extends Disposable implements IProgressService { dispose(disposables); }); + if (options.title) { + updateDialog(options.title); + } + return promise; } } diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index 5bf9929f57..eac2d98ce7 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -10,7 +11,7 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { ALL_INTERFACES_ADDRESSES, isAllInterfaces, isLocalhost, ITunnelService, LOCALHOST_ADDRESSES, PortAttributesProvider, ProvidedOnAutoForward, ProvidedPortAttributes, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IEditableData } from 'vs/workbench/common/views'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TunnelInformation, TunnelDescription, IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection'; @@ -21,6 +22,10 @@ import { hash } from 'vs/base/common/hash'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { flatten } from 'vs/base/common/arrays'; +import Severity from 'vs/base/common/severity'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { URI } from 'vs/base/common/uri'; +import { deepClone } from 'vs/base/common/objects'; export const IRemoteExplorerService = createDecorator('remoteExplorerService'); export const REMOTE_EXPLORER_TYPE_KEY: string = 'remote.explorerType'; @@ -50,6 +55,8 @@ export interface ITunnelItem { remoteHost: string; remotePort: number; localAddress?: string; + protocol: TunnelProtocol; + localUri?: URI; localPort?: number; name?: string; closeable?: boolean; @@ -71,6 +78,8 @@ export interface Tunnel { remoteHost: string; remotePort: number; localAddress: string; + localUri: URI; + protocol: TunnelProtocol; localPort?: number; name?: string; closeable?: boolean; @@ -141,10 +150,17 @@ export enum OnPortForward { Ignore = 'ignore' } -interface Attributes { +export enum TunnelProtocol { + Http = 'http', + Https = 'https' +} + +export interface Attributes { label: string | undefined; onAutoForward: OnPortForward | undefined, elevateIfNeeded: boolean | undefined; + requireLocalPort: boolean | undefined; + protocol: TunnelProtocol | undefined; } interface PortRange { start: number, end: number } @@ -182,7 +198,9 @@ export class PortsAttributes extends Disposable { const attributes: Attributes = { label: undefined, onAutoForward: undefined, - elevateIfNeeded: undefined + elevateIfNeeded: undefined, + requireLocalPort: undefined, + protocol: undefined }; while (index >= 0) { const found = this.portsAttributes[index]; @@ -190,15 +208,21 @@ export class PortsAttributes extends Disposable { attributes.onAutoForward = found.onAutoForward ?? attributes.onAutoForward; attributes.elevateIfNeeded = (found.elevateIfNeeded !== undefined) ? found.elevateIfNeeded : attributes.elevateIfNeeded; attributes.label = found.label ?? attributes.label; + attributes.requireLocalPort = found.requireLocalPort; + attributes.protocol = found.protocol; } else { // It's a range or regex, which means that if the attribute is already set, we keep it attributes.onAutoForward = attributes.onAutoForward ?? found.onAutoForward; attributes.elevateIfNeeded = (attributes.elevateIfNeeded !== undefined) ? attributes.elevateIfNeeded : found.elevateIfNeeded; attributes.label = attributes.label ?? found.label; + attributes.requireLocalPort = (attributes.requireLocalPort !== undefined) ? attributes.requireLocalPort : undefined; + attributes.protocol = attributes.protocol ?? found.protocol; } index = this.findNextIndex(port, commandLine, this.portsAttributes, index + 1); } - if (attributes.onAutoForward !== undefined || attributes.elevateIfNeeded !== undefined || attributes.label !== undefined) { + if (attributes.onAutoForward !== undefined || attributes.elevateIfNeeded !== undefined + || attributes.label !== undefined || attributes.requireLocalPort !== undefined + || attributes.protocol !== undefined) { return attributes; } @@ -263,9 +287,11 @@ export class PortsAttributes extends Disposable { } attributes.push({ key: key, - elevateIfNeeded: setting.elevateIfPrivileged, + elevateIfNeeded: setting.elevateIfNeeded, onAutoForward: setting.onAutoForward, - label: setting.label + label: setting.label, + requireLocalPort: setting.requireLocalPort, + protocol: setting.protocol }); } @@ -274,7 +300,9 @@ export class PortsAttributes extends Disposable { this.defaultPortAttributes = { elevateIfNeeded: defaults.elevateIfNeeded, label: defaults.label, - onAutoForward: defaults.onAutoForward + onAutoForward: defaults.onAutoForward, + requireLocalPort: defaults.requireLocalPort, + protocol: defaults.protocol }; } @@ -311,10 +339,33 @@ export class PortsAttributes extends Disposable { default: return undefined; } } + + public async addAttributes(port: number, attributes: Partial) { + let settingValue = this.configurationService.inspect(PortsAttributes.SETTING); + const userValue: any = settingValue.userLocalValue; + let newUserValue: any; + if (!userValue || !isObject(userValue)) { + newUserValue = {}; + } else { + newUserValue = deepClone(userValue); + } + + if (!newUserValue[`${port}`]) { + newUserValue[`${port}`] = {}; + } + for (const attribute in attributes) { + newUserValue[`${port}`][attribute] = (attributes)[attribute]; + } + + return this.configurationService.updateValue(PortsAttributes.SETTING, newUserValue, ConfigurationTarget.USER_LOCAL); + } } +const MISMATCH_LOCAL_PORT_COOLDOWN = 10 * 1000; // 10 seconds + export class TunnelModel extends Disposable { readonly forwarded: Map; + private readonly inProgress: Map = new Map(); readonly detected: Map; private remoteTunnels: Map; private _onForwardPort: Emitter = new Emitter(); @@ -332,7 +383,7 @@ export class TunnelModel extends Disposable { private _onEnvironmentTunnelsSet: Emitter = new Emitter(); public onEnvironmentTunnelsSet: Event = this._onEnvironmentTunnelsSet.event; private _environmentTunnelsSet: boolean = false; - private configPortsAttributes: PortsAttributes; + public readonly configPortsAttributes: PortsAttributes; private restoreListener: IDisposable | undefined; private portAttributesProviders: PortAttributesProvider[] = []; @@ -344,7 +395,8 @@ export class TunnelModel extends Disposable { @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, + @IDialogService private readonly dialogService: IDialogService ) { super(); this.configPortsAttributes = new PortsAttributes(configurationService); @@ -352,8 +404,9 @@ export class TunnelModel extends Disposable { this._register(this.configPortsAttributes.onDidChangeAttributes(this.updateAttributes, this)); this.forwarded = new Map(); this.remoteTunnels = new Map(); - this.tunnelService.tunnels.then(tunnels => { - tunnels.forEach(tunnel => { + this.tunnelService.tunnels.then(async (tunnels) => { + const attributes = await this.getAttributes(tunnels.map(tunnel => tunnel.tunnelRemotePort)); + for (const tunnel of tunnels) { if (tunnel.localAddress) { const key = makeAddress(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort); const matchingCandidate = mapHasAddressLocalhostOrAllInterfaces(this._candidates ?? new Map(), tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort); @@ -361,6 +414,8 @@ export class TunnelModel extends Disposable { remotePort: tunnel.tunnelRemotePort, remoteHost: tunnel.tunnelRemoteHost, localAddress: tunnel.localAddress, + protocol: attributes?.get(tunnel.tunnelRemotePort)?.protocol ?? TunnelProtocol.Http, + localUri: await this.makeLocalUri(tunnel.localAddress, attributes?.get(tunnel.tunnelRemotePort)), localPort: tunnel.tunnelLocalPort, runningProcess: matchingCandidate?.detail, hasRunningProcess: !!matchingCandidate, @@ -370,18 +425,23 @@ export class TunnelModel extends Disposable { }); this.remoteTunnels.set(key, tunnel); } - }); + } }); this.detected = new Map(); this._register(this.tunnelService.onTunnelOpened(async (tunnel) => { const key = makeAddress(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort); - if ((!this.forwarded.has(key)) && tunnel.localAddress) { + if (!mapHasAddressLocalhostOrAllInterfaces(this.forwarded, tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort) + && !mapHasAddressLocalhostOrAllInterfaces(this.inProgress, tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort) + && tunnel.localAddress) { const matchingCandidate = mapHasAddressLocalhostOrAllInterfaces(this._candidates ?? new Map(), tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort); + const attributes = (await this.getAttributes([tunnel.tunnelRemotePort]))?.get(tunnel.tunnelRemotePort); this.forwarded.set(key, { remoteHost: tunnel.tunnelRemoteHost, remotePort: tunnel.tunnelRemotePort, localAddress: tunnel.localAddress, + protocol: attributes?.protocol ?? TunnelProtocol.Http, + localUri: await this.makeLocalUri(tunnel.localAddress, attributes), localPort: tunnel.tunnelLocalPort, closeable: true, runningProcess: matchingCandidate?.detail, @@ -395,16 +455,28 @@ export class TunnelModel extends Disposable { this.remoteTunnels.set(key, tunnel); this._onForwardPort.fire(this.forwarded.get(key)!); })); - this._register(this.tunnelService.onTunnelClosed(async (address) => { - const key = makeAddress(address.host, address.port); - if (this.forwarded.has(key)) { - this.forwarded.delete(key); - await this.storeForwarded(); - this._onClosePort.fire(address); - } + this._register(this.tunnelService.onTunnelClosed(address => { + return this.onTunnelClosed(address); })); } + private async onTunnelClosed(address: { host: string, port: number }) { + const key = makeAddress(address.host, address.port); + if (this.forwarded.has(key)) { + this.forwarded.delete(key); + await this.storeForwarded(); + this._onClosePort.fire(address); + } + } + + private makeLocalUri(localAddress: string, attributes?: Attributes) { + if (localAddress.startsWith('http')) { + return URI.parse(localAddress); + } + const protocol = attributes?.protocol ?? 'http'; + return URI.parse(`${protocol}://${localAddress}`); + } + private makeTunnelPrivacy(isPublic: boolean) { return isPublic ? TunnelPrivacy.Public : this.tunnelService.canMakePublic ? TunnelPrivacy.Private : TunnelPrivacy.ConstantPrivate; } @@ -458,10 +530,30 @@ export class TunnelModel extends Disposable { } } - async forward(remote: { host: string, port: number }, local?: number, name?: string, source?: string, elevateIfNeeded?: boolean, isPublic?: boolean, restore: boolean = true): Promise { + private mismatchCooldown = new Date(); + private async showPortMismatchModalIfNeeded(tunnel: RemoteTunnel, expectedLocal: number, attributes: Attributes | undefined) { + if (!tunnel.tunnelLocalPort || !attributes?.requireLocalPort) { + return undefined; // {{SQL CARBON EDIT}} Strict nulls + } + if (tunnel.tunnelLocalPort === expectedLocal) { + return undefined; // {{SQL CARBON EDIT}} Strict nulls + } + + const newCooldown = new Date(); + if ((this.mismatchCooldown.getTime() + MISMATCH_LOCAL_PORT_COOLDOWN) > newCooldown.getTime()) { + return undefined; // {{SQL CARBON EDIT}} Strict nulls + } + this.mismatchCooldown = newCooldown; + const mismatchString = nls.localize('remote.localPortMismatch.single', "Local port {0} could not be used for forwarding to remote port {1}.\n\nThis usually happens when there is already another process using local port {0}.\n\nPort number {2} has been used instead.", + expectedLocal, tunnel.tunnelRemotePort, tunnel.tunnelLocalPort); + return this.dialogService.show(Severity.Info, mismatchString); + } + + async forward(remote: { host: string, port: number }, local?: number, name?: string, source?: string, elevateIfNeeded?: boolean, + isPublic?: boolean, restore: boolean = true, attributes?: Attributes | null): Promise { const existingTunnel = mapHasAddressLocalhostOrAllInterfaces(this.forwarded, remote.host, remote.port); - const port = local !== undefined ? local : remote.port; - const attributes = (await this.getAttributes([port]))?.get(port); + attributes = attributes ?? ((attributes !== null) ? (await this.getAttributes([remote.port]))?.get(remote.port) : undefined); + const localPort = (local !== undefined) ? local : remote.port; if (!existingTunnel) { const authority = this.environmentService.remoteAuthority; @@ -469,7 +561,9 @@ export class TunnelModel extends Disposable { getAddress: async () => { return (await this.remoteAuthorityResolverService.resolveAuthority(authority)).authority; } } : undefined; - const tunnel = await this.tunnelService.openTunnel(addressProvider, remote.host, remote.port, local, (!elevateIfNeeded) ? attributes?.elevateIfNeeded : elevateIfNeeded, isPublic); + const key = makeAddress(remote.host, remote.port); + this.inProgress.set(key, true); + const tunnel = await this.tunnelService.openTunnel(addressProvider, remote.host, remote.port, localPort, (!elevateIfNeeded) ? attributes?.elevateIfNeeded : elevateIfNeeded, isPublic); if (tunnel && tunnel.localAddress) { const matchingCandidate = mapHasAddressLocalhostOrAllInterfaces(this._candidates ?? new Map(), remote.host, remote.port); const newForward: Tunnel = { @@ -479,6 +573,8 @@ export class TunnelModel extends Disposable { name: attributes?.label ?? name, closeable: true, localAddress: tunnel.localAddress, + protocol: attributes?.protocol ?? TunnelProtocol.Http, + localUri: await this.makeLocalUri(tunnel.localAddress, attributes), runningProcess: matchingCandidate?.detail, hasRunningProcess: !!matchingCandidate, pid: matchingCandidate?.pid, @@ -486,18 +582,24 @@ export class TunnelModel extends Disposable { privacy: this.makeTunnelPrivacy(tunnel.public), userForwarded: restore }; - const key = makeAddress(remote.host, remote.port); this.forwarded.set(key, newForward); this.remoteTunnels.set(key, tunnel); + this.inProgress.delete(key); await this.storeForwarded(); + await this.showPortMismatchModalIfNeeded(tunnel, localPort, attributes); this._onForwardPort.fire(newForward); return tunnel; } } else { if (attributes?.label ?? name) { existingTunnel.name = attributes?.label ?? name; + this._onForwardPort.fire(); + } + // Remove tunnel provider check when protocol is part of the API https://github.com/microsoft/vscode/issues/124816 + if (!this.tunnelService.hasTunnelProvider && attributes?.protocol && (attributes.protocol !== existingTunnel.protocol)) { + await this.close(existingTunnel.remoteHost, existingTunnel.remotePort); + await this.forward({ host: existingTunnel.remoteHost, port: existingTunnel.remotePort }, local, name, source, elevateIfNeeded, isPublic, restore, attributes); } - this._onForwardPort.fire(); return mapHasAddressLocalhostOrAllInterfaces(this.remoteTunnels, remote.host, remote.port); } } @@ -517,7 +619,8 @@ export class TunnelModel extends Disposable { } async close(host: string, port: number): Promise { - return this.tunnelService.closeTunnel(host, port); + await this.tunnelService.closeTunnel(host, port); + return this.onTunnelClosed({ host, port }); } address(host: string, port: number): string | undefined { @@ -531,12 +634,15 @@ export class TunnelModel extends Disposable { addEnvironmentTunnels(tunnels: TunnelDescription[] | undefined): void { if (tunnels) { - tunnels.forEach(tunnel => { + for (const tunnel of tunnels) { const matchingCandidate = mapHasAddressLocalhostOrAllInterfaces(this._candidates ?? new Map(), tunnel.remoteAddress.host, tunnel.remoteAddress.port); + const localAddress = typeof tunnel.localAddress === 'string' ? tunnel.localAddress : makeAddress(tunnel.localAddress.host, tunnel.localAddress.port); this.detected.set(makeAddress(tunnel.remoteAddress.host, tunnel.remoteAddress.port), { remoteHost: tunnel.remoteAddress.host, remotePort: tunnel.remoteAddress.port, - localAddress: typeof tunnel.localAddress === 'string' ? tunnel.localAddress : makeAddress(tunnel.localAddress.host, tunnel.localAddress.port), + localAddress: localAddress, + protocol: TunnelProtocol.Http, + localUri: this.makeLocalUri(localAddress), closeable: false, runningProcess: matchingCandidate?.detail, hasRunningProcess: !!matchingCandidate, @@ -544,7 +650,7 @@ export class TunnelModel extends Disposable { privacy: TunnelPrivacy.ConstantPrivate, userForwarded: false }); - }); + } } this._environmentTunnelsSet = true; this._onEnvironmentTunnelsSet.fire(); @@ -621,11 +727,22 @@ export class TunnelModel extends Disposable { private async updateAttributes() { // If the label changes in the attributes, we should update it. - for (let forwarded of this.forwarded.values()) { - const attributes = (await this.getAttributes([forwarded.remotePort], false))?.get(forwarded.remotePort); - if (attributes && attributes.label && attributes.label !== forwarded.name) { + const tunnels = Array.from(this.forwarded.values()); + const allAttributes = await this.getAttributes(tunnels.map(tunnel => tunnel.remotePort), false); + if (!allAttributes) { + return; + } + for (const forwarded of tunnels) { + const attributes = allAttributes.get(forwarded.remotePort); + if (!attributes) { + continue; + } + if (attributes.label && attributes.label !== forwarded.name) { await this.name(forwarded.remoteHost, forwarded.remotePort, attributes.label); } + if (attributes.protocol && attributes.protocol !== forwarded.protocol) { + await this.forward({ host: forwarded.remoteHost, port: forwarded.remotePort }, forwarded.localPort, forwarded.name, forwarded.source, undefined, undefined, undefined, attributes); + } } } @@ -682,7 +799,9 @@ export class TunnelModel extends Disposable { mergedAttributes.set(port, { elevateIfNeeded: config?.elevateIfNeeded, label: config?.label, - onAutoForward: config?.onAutoForward ?? PortsAttributes.providedActionToAction(provider?.autoForwardAction) + onAutoForward: config?.onAutoForward ?? PortsAttributes.providedActionToAction(provider?.autoForwardAction), + requireLocalPort: config?.requireLocalPort, + protocol: config?.protocol }); }); @@ -709,7 +828,7 @@ export interface IRemoteExplorerService { onDidChangeEditable: Event<{ tunnel: ITunnelItem, editId: TunnelEditId } | undefined>; setEditable(tunnelItem: ITunnelItem | undefined, editId: TunnelEditId, data: IEditableData | null): void; getEditableData(tunnelItem: ITunnelItem | undefined, editId?: TunnelEditId): IEditableData | undefined; - forward(remote: { host: string, port: number }, localPort?: number, name?: string, source?: string, elevateIfNeeded?: boolean, isPublic?: boolean, restore?: boolean): Promise; + forward(remote: { host: string, port: number }, localPort?: number, name?: string, source?: string, elevateIfNeeded?: boolean, isPublic?: boolean, restore?: boolean, attributes?: Attributes | null): Promise; close(remote: { host: string, port: number }): Promise; setTunnelInformation(tunnelInformation: TunnelInformation | undefined): void; setCandidateFilter(filter: ((candidates: CandidatePort[]) => Promise) | undefined): IDisposable; @@ -742,9 +861,11 @@ class RemoteExplorerService implements IRemoteExplorerService { @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, - @ILogService logService: ILogService + @ILogService logService: ILogService, + @IDialogService dialogService: IDialogService ) { - this._tunnelModel = new TunnelModel(tunnelService, storageService, configurationService, environmentService, remoteAuthorityResolverService, workspaceContextService, logService); + this._tunnelModel = new TunnelModel(tunnelService, storageService, configurationService, environmentService, + remoteAuthorityResolverService, workspaceContextService, logService, dialogService); } set targetType(name: string[]) { @@ -766,8 +887,8 @@ class RemoteExplorerService implements IRemoteExplorerService { return this._tunnelModel; } - forward(remote: { host: string, port: number }, local?: number, name?: string, source?: string, elevateIfNeeded?: boolean, isPublic?: boolean, restore?: boolean): Promise { - return this.tunnelModel.forward(remote, local, name, source, elevateIfNeeded, isPublic, restore); + forward(remote: { host: string, port: number }, local?: number, name?: string, source?: string, elevateIfNeeded?: boolean, isPublic?: boolean, restore?: boolean, attributes?: Attributes | null): Promise { + return this.tunnelModel.forward(remote, local, name, source, elevateIfNeeded, isPublic, restore, attributes); } close(remote: { host: string, port: number }): Promise { diff --git a/src/vs/workbench/services/remote/electron-browser/tunnelServiceImpl.ts b/src/vs/workbench/services/remote/electron-browser/tunnelServiceImpl.ts index 0d397b6b92..494afcc2cb 100644 --- a/src/vs/workbench/services/remote/electron-browser/tunnelServiceImpl.ts +++ b/src/vs/workbench/services/remote/electron-browser/tunnelServiceImpl.ts @@ -13,6 +13,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { URI } from 'vs/base/common/uri'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export class TunnelService extends BaseTunnelService { public constructor( @@ -20,9 +21,10 @@ export class TunnelService extends BaseTunnelService { @ISignService signService: ISignService, @IProductService productService: IProductService, @IRemoteAgentService _remoteAgentService: IRemoteAgentService, - @IWorkbenchEnvironmentService private environmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService private environmentService: IWorkbenchEnvironmentService, + @IConfigurationService configurationService: IConfigurationService ) { - super(nodeSocketFactory, logService, signService, productService); + super(nodeSocketFactory, logService, signService, productService, configurationService); } override canTunnel(uri: URI): boolean { diff --git a/src/vs/workbench/services/remote/test/common/testServices.ts b/src/vs/workbench/services/remote/test/common/testServices.ts index 1637e19d71..beb2d2535a 100644 --- a/src/vs/workbench/services/remote/test/common/testServices.ts +++ b/src/vs/workbench/services/remote/test/common/testServices.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/services/search/browser/searchService.ts b/src/vs/workbench/services/search/browser/searchService.ts index 8284b1b0e1..d5f2c54a7c 100644 --- a/src/vs/workbench/services/search/browser/searchService.ts +++ b/src/vs/workbench/services/search/browser/searchService.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IModelService } from 'vs/editor/common/services/modelService'; diff --git a/src/vs/workbench/services/search/common/replace.ts b/src/vs/workbench/services/search/common/replace.ts index a9e627f7a6..688f69fae4 100644 --- a/src/vs/workbench/services/search/common/replace.ts +++ b/src/vs/workbench/services/search/common/replace.ts @@ -39,7 +39,7 @@ export class ReplacePattern { this._regExp = strings.createRegExp(this._regExp.source, true, { matchCase: !this._regExp.ignoreCase, wholeWord: false, multiline: this._regExp.multiline, global: false }); } - this._caseOpsRegExp = new RegExp(/([^\\]*?)((?:\\[uUlL])+?|)(\$[0-9]+)(.*?)/g); + this._caseOpsRegExp = new RegExp(/(.*?)((?:\\[uUlL])+?|)(\$[0-9]+)(.*?)/g); } get hasParameters(): boolean { diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index cc84d95342..624cc866e9 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -206,9 +206,15 @@ export function isProgressMessage(p: ISearchProgressItem | ISerializedSearchProg return !!(p as IProgressMessage).message; } +export interface ITextSearchCompleteMessage { + text: string; + type: TextSearchCompleteMessageType; + trusted?: boolean; +} + export interface ISearchCompleteStats { limitHit?: boolean; - messages: { text: string, type: TextSearchCompleteMessageType }[]; + messages: ITextSearchCompleteMessage[]; stats?: IFileSearchStats | ITextSearchStats; } @@ -508,13 +514,13 @@ export interface ISearchEngine { export interface ISerializedSearchSuccess { type: 'success'; limitHit: boolean; - messages: { text: string, type: TextSearchCompleteMessageType }[]; + messages: ITextSearchCompleteMessage[]; stats?: IFileSearchStats | ITextSearchStats; } export interface ISearchEngineSuccess { limitHit: boolean; - messages: { text: string, type: TextSearchCompleteMessageType }[]; + messages: ITextSearchCompleteMessage[]; stats: ISearchEngineStats; } diff --git a/src/vs/workbench/services/search/common/searchExtTypes.ts b/src/vs/workbench/services/search/common/searchExtTypes.ts index 1cf8d31e15..ec245b8a55 100644 --- a/src/vs/workbench/services/search/common/searchExtTypes.ts +++ b/src/vs/workbench/services/search/common/searchExtTypes.ts @@ -224,6 +224,24 @@ export enum TextSearchCompleteMessageType { Warning = 2, } +/** + * A message regarding a completed search. + */ +export interface TextSearchCompleteMessage { + /** + * Markdown text of the message. + */ + text: string, + /** + * Whether the source of the message is trusted, command links are disabled for untrusted message sources. + */ + trusted?: boolean, + /** + * The message type, this affects how the message will be rendered. + */ + type: TextSearchCompleteMessageType, +} + /** * Information collected when text search is complete. */ @@ -244,7 +262,7 @@ export interface TextSearchComplete { * - Click to [run a command](command:workbench.action.OpenQuickPick) * - Click to [open a website](https://aka.ms) */ - message?: { text: string, type: TextSearchCompleteMessageType } | { text: string, type: TextSearchCompleteMessageType }[]; + message?: TextSearchCompleteMessage | TextSearchCompleteMessage[]; } /** diff --git a/src/vs/workbench/services/search/common/searchHelpers.ts b/src/vs/workbench/services/search/common/searchHelpers.ts index 3d4c0f9d44..16d91584dc 100644 --- a/src/vs/workbench/services/search/common/searchHelpers.ts +++ b/src/vs/workbench/services/search/common/searchHelpers.ts @@ -89,4 +89,4 @@ function getMatchStartEnd(match: ITextSearchMatch): { start: number, end: number start: matchStartLine, end: matchEndLine }; -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/search/common/searchService.ts b/src/vs/workbench/services/search/common/searchService.ts index 6cab8cc80b..0aaf69de78 100644 --- a/src/vs/workbench/services/search/common/searchService.ts +++ b/src/vs/workbench/services/search/common/searchService.ts @@ -153,7 +153,7 @@ export class SearchService extends Disposable implements ISearchService { return { limitHit: completes[0] && completes[0].limitHit, stats: completes[0].stats, - messages: arrays.coalesce(arrays.flatten(completes.map(i => i.messages))).filter(arrays.uniqueFilter(message => message.type + message.text)), + messages: arrays.coalesce(arrays.flatten(completes.map(i => i.messages))).filter(arrays.uniqueFilter(message => message.type + message.text + message.trusted)), results: arrays.flatten(completes.map((c: ISearchComplete) => c.results)) }; })(); diff --git a/src/vs/workbench/services/search/electron-browser/searchService.ts b/src/vs/workbench/services/search/electron-browser/searchService.ts index 936998dddc..6f19c559d4 100644 --- a/src/vs/workbench/services/search/electron-browser/searchService.ts +++ b/src/vs/workbench/services/search/electron-browser/searchService.ts @@ -27,6 +27,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { FileAccess } from 'vs/base/common/network'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; export class LocalSearchService extends SearchService { constructor( @@ -54,6 +55,7 @@ export class DiskSearch implements ISearchResultProvider { searchDebug: IDebugParams | undefined, @ILogService private readonly logService: ILogService, @IConfigurationService private readonly configService: IConfigurationService, + @ILifecycleService private readonly lifecycleService: ILifecycleService ) { const timeout = this.configService.getValue().search.maintainFileSearchCache ? 100 * 60 * 60 * 1000 : @@ -84,6 +86,8 @@ export class DiskSearch implements ISearchResultProvider { const client = new Client(FileAccess.asFileUri('bootstrap-fork', require).fsPath, opts); const channel = getNextTickChannel(client.getChannel('search')); this.raw = new SearchChannelClient(channel); + + this.lifecycleService.onWillShutdown(_ => client.dispose()); } textSearch(query: ITextQuery, onProgress?: (p: ISearchProgressItem) => void, token?: CancellationToken): Promise { diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index 37f9028f7a..dc3f2e379b 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -18,7 +18,7 @@ import { StopWatch } from 'vs/base/common/stopwatch'; import * as strings from 'vs/base/common/strings'; import * as types from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { readdir } from 'vs/base/node/pfs'; +import { Promises } from 'vs/base/node/pfs'; import { IFileQuery, IFolderQuery, IProgressMessage, ISearchEngineStats, IRawFileMatch, ISearchEngine, ISearchEngineSuccess, isFilePatternMatch } from 'vs/workbench/services/search/common/search'; import { spawnRipgrepCmd } from './ripgrepFileSearch'; import { prepareQuery } from 'vs/base/common/fuzzyScorer'; @@ -518,7 +518,7 @@ export class FileWalker { this.walkedPaths[realpath] = true; // remember as walked // Continue walking - return readdir(currentAbsolutePath).then(children => { + return Promises.readdir(currentAbsolutePath).then(children => { if (this.isCanceled || this.isLimitHit) { return clb(null); } diff --git a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts index 3ce30b2695..64472fe6f6 100644 --- a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts +++ b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts @@ -576,9 +576,15 @@ export function fixRegexNewline(pattern: string): string { } else if (context.some(isLookBehind)) { // no-op in a lookbehind, see #100569 } else if (parent.type === 'CharacterClass') { - // in a bracket expr, [a-z\n] -> (?:[a-z]|\r?\n) - const otherContent = pattern.slice(parent.start + 1, char.start) + pattern.slice(char.end, parent.end - 1); - replace(parent.start, parent.end, otherContent === '' ? '\\r?\\n' : `(?:[${otherContent}]|\\r?\\n)`); + if (parent.negate) { + // negative bracket expr, [^a-z\n] -> (?![a-z]|\r?\n) + const otherContent = pattern.slice(parent.start + 2, char.start) + pattern.slice(char.end, parent.end - 1); + replace(parent.start, parent.end, '(?!\\r?\\n' + (otherContent ? `|[${otherContent}]` : '') + ')'); + } else { + // positive bracket expr, [a-z\n] -> (?:[a-z]|\r?\n) + const otherContent = pattern.slice(parent.start + 1, char.start) + pattern.slice(char.end, parent.end - 1); + replace(parent.start, parent.end, otherContent === '' ? '\\r?\\n' : `(?:[${otherContent}]|\\r?\\n)`); + } } else if (parent.type === 'Quantifier') { replace(char.start, char.end, '(?:\\r?\\n)'); } diff --git a/src/vs/workbench/services/search/node/searchApp.ts b/src/vs/workbench/services/search/node/searchApp.ts index ba4737b5d2..cf92a6e3bf 100644 --- a/src/vs/workbench/services/search/node/searchApp.ts +++ b/src/vs/workbench/services/search/node/searchApp.ts @@ -10,4 +10,4 @@ import { SearchService } from './rawSearchService'; const server = new Server('search'); const service = new SearchService(); const channel = new SearchChannel(service); -server.registerChannel('search', channel); \ No newline at end of file +server.registerChannel('search', channel); diff --git a/src/vs/workbench/services/search/node/searchIpc.ts b/src/vs/workbench/services/search/node/searchIpc.ts index 2c15b8e4c9..0aa773ab64 100644 --- a/src/vs/workbench/services/search/node/searchIpc.ts +++ b/src/vs/workbench/services/search/node/searchIpc.ts @@ -42,4 +42,4 @@ export class SearchChannelClient implements IRawSearchService { clearCache(cacheKey: string): Promise { return this.channel.call('clearCache', cacheKey); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/search/node/textSearchManager.ts b/src/vs/workbench/services/search/node/textSearchManager.ts index 1724dd2307..1b1621e164 100644 --- a/src/vs/workbench/services/search/node/textSearchManager.ts +++ b/src/vs/workbench/services/search/node/textSearchManager.ts @@ -13,7 +13,7 @@ export class NativeTextSearchManager extends TextSearchManager { constructor(query: ITextQuery, provider: TextSearchProvider, _pfs: typeof pfs = pfs) { super(query, provider, { - readdir: resource => _pfs.readdir(resource.fsPath), + readdir: resource => _pfs.Promises.readdir(resource.fsPath), toCanonicalName: name => toCanonicalName(name) }); } diff --git a/src/vs/workbench/services/search/test/common/replace.test.ts b/src/vs/workbench/services/search/test/common/replace.test.ts index d86efbf0dc..835c41e7d4 100644 --- a/src/vs/workbench/services/search/test/common/replace.test.ts +++ b/src/vs/workbench/services/search/test/common/replace.test.ts @@ -146,6 +146,20 @@ suite('Replace Pattern test', () => { assert.strictEqual('aLlGoODMen', actual); }); + test('case operations - no false positive', () => { + let testObject = new ReplacePattern('\\left $1', { pattern: '(pattern)', isRegExp: true }); + let actual = testObject.getReplaceString('pattern'); + assert.strictEqual('\\left pattern', actual); + + testObject = new ReplacePattern('\\hi \\left $1', { pattern: '(pattern)', isRegExp: true }); + actual = testObject.getReplaceString('pattern'); + assert.strictEqual('\\hi \\left pattern', actual); + + testObject = new ReplacePattern('\\left \\L$1', { pattern: 'PATT(ERN)', isRegExp: true }); + actual = testObject.getReplaceString('PATTERN'); + assert.strictEqual('\\left ern', actual); + }); + test('get replace string for no matches', () => { let testObject = new ReplacePattern('hello', { pattern: 'bla', isRegExp: true }); let actual = testObject.getReplaceString('foo'); diff --git a/src/vs/workbench/services/search/test/common/searchHelpers.test.ts b/src/vs/workbench/services/search/test/common/searchHelpers.test.ts index 57345ca8cf..dccf74088d 100644 --- a/src/vs/workbench/services/search/test/common/searchHelpers.test.ts +++ b/src/vs/workbench/services/search/test/common/searchHelpers.test.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ITextModel, FindMatch } from 'vs/editor/common/model'; -import { editorMatchesToTextSearchResults, addContextToEditorMatches } from 'vs/workbench/services/search/common/searchHelpers'; import { Range } from 'vs/editor/common/core/range'; -import { ITextQuery, QueryType, ITextSearchContext } from 'vs/workbench/services/search/common/search'; +import { FindMatch, ITextModel } from 'vs/editor/common/model'; +import { ISearchRange, ITextQuery, ITextSearchContext, QueryType } from 'vs/workbench/services/search/common/search'; +import { addContextToEditorMatches, editorMatchesToTextSearchResults } from 'vs/workbench/services/search/common/searchHelpers'; suite('SearchHelpers', () => { suite('editorMatchesToTextSearchResults', () => { @@ -17,12 +17,29 @@ suite('SearchHelpers', () => { } }; + function assertRangesEqual(actual: ISearchRange | ISearchRange[], expected: ISearchRange[]) { + if (!Array.isArray(actual)) { + // All of these tests are for arrays... + throw new Error('Expected array of ranges'); + } + + assert.strictEqual(actual.length, expected.length); + + // These are sometimes Range, sometimes SearchRange + actual.forEach((r, i) => { + const expectedRange = expected[i]; + assert.deepStrictEqual( + { startLineNumber: r.startLineNumber, startColumn: r.startColumn, endLineNumber: r.endLineNumber, endColumn: r.endColumn }, + { startLineNumber: expectedRange.startLineNumber, startColumn: expectedRange.startColumn, endLineNumber: expectedRange.endLineNumber, endColumn: expectedRange.endColumn }); + }); + } + test('simple', () => { const results = editorMatchesToTextSearchResults([new FindMatch(new Range(6, 1, 6, 2), null)], mockTextModel); assert.strictEqual(results.length, 1); assert.strictEqual(results[0].preview.text, '6\n'); - assert.deepEqual(results[0].preview.matches, [new Range(0, 0, 0, 1)]); - assert.deepEqual(results[0].ranges, [new Range(5, 0, 5, 1)]); + assertRangesEqual(results[0].preview.matches, [new Range(0, 0, 0, 1)]); + assertRangesEqual(results[0].ranges, [new Range(5, 0, 5, 1)]); }); test('multiple', () => { @@ -34,20 +51,20 @@ suite('SearchHelpers', () => { ], mockTextModel); assert.strictEqual(results.length, 2); - assert.deepEqual(results[0].preview.matches, [ + assertRangesEqual(results[0].preview.matches, [ new Range(0, 0, 0, 1), new Range(0, 3, 2, 1), ]); - assert.deepEqual(results[0].ranges, [ + assertRangesEqual(results[0].ranges, [ new Range(5, 0, 5, 1), new Range(5, 3, 7, 1), ]); assert.strictEqual(results[0].preview.text, '6\n7\n8\n'); - assert.deepEqual(results[1].preview.matches, [ + assertRangesEqual(results[1].preview.matches, [ new Range(0, 0, 1, 2), ]); - assert.deepEqual(results[1].ranges, [ + assertRangesEqual(results[1].ranges, [ new Range(8, 0, 9, 2), ]); assert.strictEqual(results[1].preview.text, '9\n10\n'); diff --git a/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts b/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts index fb7029dc46..ed793d4d8c 100644 --- a/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts +++ b/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts @@ -38,6 +38,8 @@ suite('RipgrepTextSearchEngine', () => { ['f[\\n-a]', 'f[\\n-a]'], ['(?<=\\n)\\w', '(?<=\\n)\\w'], ['fo\\n+o', 'fo(?:\\r?\\n)+o'], + ['fo[^\\n]o', 'fo(?!\\r?\\n)o'], + ['fo[^\\na-z]o', 'fo(?!\\r?\\n|[a-z])o'], ]; for (const [input, expected] of ttable) { diff --git a/src/vs/workbench/services/statusbar/common/statusbar.ts b/src/vs/workbench/services/statusbar/common/statusbar.ts index e223e678a8..7d9782734f 100644 --- a/src/vs/workbench/services/statusbar/common/statusbar.ts +++ b/src/vs/workbench/services/statusbar/common/statusbar.ts @@ -8,6 +8,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { Event } from 'vs/base/common/event'; import { Command } from 'vs/editor/common/modes'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; export const IStatusbarService = createDecorator('statusbarService'); @@ -21,6 +22,12 @@ export const enum StatusbarAlignment { */ export interface IStatusbarEntry { + /** + * The (short) name to show for the entry like 'Language Indicator', + * 'Git Status' etc. + */ + readonly name: string; + /** * The text to show for the entry. You can embed icons in the text by leveraging the syntax: * @@ -42,7 +49,7 @@ export interface IStatusbarEntry { /** * An optional tooltip text to show when you hover over the entry */ - readonly tooltip?: string; + readonly tooltip?: string | IMarkdownString; /** * An optional color to use for the entry @@ -68,6 +75,7 @@ export interface IStatusbarEntry { * Will enable a spinning icon in front of the text to indicate progress. */ readonly showProgress?: boolean; + } export interface IStatusbarService { @@ -79,12 +87,11 @@ export interface IStatusbarService { * to update or remove the statusbar entry. * * @param id identifier of the entry is needed to allow users to hide entries via settings - * @param name human readable name the entry is about * @param alignment either LEFT or RIGHT * @param priority items get arranged from highest priority to lowest priority from left to right * in their respective alignment slot */ - addEntry(entry: IStatusbarEntry, id: string, name: string, alignment: StatusbarAlignment, priority?: number): IStatusbarEntryAccessor; + addEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priority?: number): IStatusbarEntryAccessor; /** * An event that is triggered when an entry's visibility is changed. diff --git a/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts b/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts index e50486321c..4a407f8ba7 100644 --- a/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts +++ b/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts @@ -10,7 +10,7 @@ import { release, tmpdir, hostname } from 'os'; import { resolveWorkbenchCommonProperties } from 'vs/workbench/services/telemetry/electron-sandbox/workbenchCommonProperties'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { IStorageService, StorageScope, InMemoryStorageService, StorageTarget } from 'vs/platform/storage/common/storage'; -import { rimraf } from 'vs/base/node/pfs'; +import { Promises } from 'vs/base/node/pfs'; import { timeout } from 'vs/base/common/async'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; @@ -40,11 +40,11 @@ suite('Telemetry - common properties', function () { teardown(() => { diskFileSystemProvider.dispose(); - return rimraf(parentDir); + return Promises.rm(parentDir); }); test('default', async function () { - await fs.promises.mkdir(parentDir, { recursive: true }); + await Promises.mkdir(parentDir, { recursive: true }); fs.writeFileSync(installSource, 'my.install.source'); const props = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), hostname(), commit, version, 'someMachineId', undefined, installSource); assert.ok('commitHash' in props); diff --git a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts index d33d92ed79..7c11d89eeb 100644 --- a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts +++ b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts @@ -18,8 +18,7 @@ import { nullTokenize2 } from 'vs/editor/common/modes/nullMode'; import { generateTokensCSSForColorMap } from 'vs/editor/common/modes/supports/tokenization'; import { IModeService } from 'vs/editor/common/services/modeService'; import { ILogService } from 'vs/platform/log/common/log'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { INotificationService } from 'vs/platform/notification/common/notification'; import { ExtensionMessageCollector } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { ITMSyntaxExtensionPoint, grammarsExtPoint } from 'vs/workbench/services/textMate/common/TMGrammars'; import { ITextMateService } from 'vs/workbench/services/textMate/common/textMateService'; @@ -58,7 +57,6 @@ export abstract class AbstractTextMateService extends Disposable implements ITex @INotificationService private readonly _notificationService: INotificationService, @ILogService private readonly _logService: ILogService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @IStorageService private readonly _storageService: IStorageService, @IProgressService private readonly _progressService: IProgressService ) { super(); @@ -283,7 +281,7 @@ export abstract class AbstractTextMateService extends Disposable implements ITex this._onDidEncounterLanguage.fire(languageId); } }); - return new TMTokenizationSupport(r.languageId, tokenization, this._notificationService, this._configurationService, this._storageService); + return new TMTokenizationSupport(r.languageId, tokenization, this._configurationService); } catch (err) { onUnexpectedError(err); return null; @@ -415,24 +413,18 @@ export abstract class AbstractTextMateService extends Disposable implements ITex protected abstract _loadVSCodeOnigurumWASM(): Promise; } -const donotAskUpdateKey = 'editor.maxTokenizationLineLength.donotask'; - class TMTokenizationSupport implements ITokenizationSupport { private readonly _languageId: LanguageId; private readonly _actual: TMTokenization; - private _tokenizationWarningAlreadyShown: boolean; private _maxTokenizationLineLength: number; constructor( languageId: LanguageId, actual: TMTokenization, - @INotificationService private readonly _notificationService: INotificationService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @IStorageService private readonly _storageService: IStorageService ) { this._languageId = languageId; this._actual = actual; - this._tokenizationWarningAlreadyShown = !!(this._storageService.getBoolean(donotAskUpdateKey, StorageScope.GLOBAL)); this._maxTokenizationLineLength = this._configurationService.getValue('editor.maxTokenizationLineLength'); this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('editor.maxTokenizationLineLength')) { @@ -456,19 +448,6 @@ class TMTokenizationSupport implements ITokenizationSupport { // Do not attempt to tokenize if a line is too long if (line.length >= this._maxTokenizationLineLength) { - if (!this._tokenizationWarningAlreadyShown) { - this._tokenizationWarningAlreadyShown = true; - this._notificationService.prompt( - Severity.Warning, - nls.localize('too many characters', "Tokenization is skipped for long lines for performance reasons. The length of a long line can be configured via `editor.maxTokenizationLineLength`."), - [{ - label: nls.localize('neverAgain', "Don't Show Again"), - isSecondary: true, - run: () => this._storageService.store(donotAskUpdateKey, true, StorageScope.GLOBAL, StorageTarget.USER) - }] - ); - } - console.log(`Line (${line.substr(0, 15)}...): longer than ${this._maxTokenizationLineLength} characters, tokenization skipped.`); return nullTokenize2(this._languageId, line, state, offsetDelta); } diff --git a/src/vs/workbench/services/textMate/browser/textMateService.ts b/src/vs/workbench/services/textMate/browser/textMateService.ts index 90adc9543c..4c80ef1022 100644 --- a/src/vs/workbench/services/textMate/browser/textMateService.ts +++ b/src/vs/workbench/services/textMate/browser/textMateService.ts @@ -6,31 +6,9 @@ import { ITextMateService } from 'vs/workbench/services/textMate/common/textMateService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { AbstractTextMateService } from 'vs/workbench/services/textMate/browser/abstractTextMateService'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { ILogService } from 'vs/platform/log/common/log'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader'; -import { IProgressService } from 'vs/platform/progress/common/progress'; import { FileAccess } from 'vs/base/common/network'; export class TextMateService extends AbstractTextMateService { - - constructor( - @IModeService modeService: IModeService, - @IWorkbenchThemeService themeService: IWorkbenchThemeService, - @IExtensionResourceLoaderService extensionResourceLoaderService: IExtensionResourceLoaderService, - @INotificationService notificationService: INotificationService, - @ILogService logService: ILogService, - @IConfigurationService configurationService: IConfigurationService, - @IStorageService storageService: IStorageService, - @IProgressService progressService: IProgressService - ) { - super(modeService, themeService, extensionResourceLoaderService, notificationService, logService, configurationService, storageService, progressService); - } - protected async _loadVSCodeOnigurumWASM(): Promise { const response = await fetch(FileAccess.asBrowserUri('vscode-oniguruma/../onig.wasm', require).toString(true)); // Using the response directly only works if the server sets the MIME type 'application/wasm'. diff --git a/src/vs/workbench/services/textMate/electron-sandbox/textMateService.ts b/src/vs/workbench/services/textMate/electron-sandbox/textMateService.ts index 992a9bcb6e..6654e71830 100644 --- a/src/vs/workbench/services/textMate/electron-sandbox/textMateService.ts +++ b/src/vs/workbench/services/textMate/electron-sandbox/textMateService.ts @@ -22,7 +22,6 @@ import { UriComponents, URI } from 'vs/base/common/uri'; import { MultilineTokensBuilder } from 'vs/editor/common/model/tokensStore'; import { TMGrammarFactory } from 'vs/workbench/services/textMate/common/TMGrammarFactory'; import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; -import { IStorageService } from 'vs/platform/storage/common/storage'; import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IProgressService } from 'vs/platform/progress/common/progress'; @@ -148,12 +147,11 @@ export class TextMateService extends AbstractTextMateService { @INotificationService notificationService: INotificationService, @ILogService logService: ILogService, @IConfigurationService configurationService: IConfigurationService, - @IStorageService storageService: IStorageService, @IProgressService progressService: IProgressService, @IModelService private readonly _modelService: IModelService, @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, ) { - super(modeService, themeService, extensionResourceLoaderService, notificationService, logService, configurationService, storageService, progressService); + super(modeService, themeService, extensionResourceLoaderService, notificationService, logService, configurationService, progressService); this._worker = null; this._workerProxy = null; this._tokenizers = Object.create(null); diff --git a/src/vs/workbench/services/textfile/browser/browserTextFileService.ts b/src/vs/workbench/services/textfile/browser/browserTextFileService.ts index 7312597709..84f7cfd187 100644 --- a/src/vs/workbench/services/textfile/browser/browserTextFileService.ts +++ b/src/vs/workbench/services/textfile/browser/browserTextFileService.ts @@ -6,11 +6,52 @@ import { AbstractTextFileService } from 'vs/workbench/services/textfile/browser/textFileService'; import { ITextFileService, TextFileEditorModelState } from 'vs/workbench/services/textfile/common/textfiles'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IElevatedFileService } from 'vs/workbench/services/files/common/elevatedFileService'; +import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; export class BrowserTextFileService extends AbstractTextFileService { - protected override registerListeners(): void { - super.registerListeners(); + constructor( + @IFileService fileService: IFileService, + @IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService, + @ILifecycleService lifecycleService: ILifecycleService, + @IInstantiationService instantiationService: IInstantiationService, + @IModelService modelService: IModelService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IDialogService dialogService: IDialogService, + @IFileDialogService fileDialogService: IFileDialogService, + @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, + @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, + @ITextModelService textModelService: ITextModelService, + @ICodeEditorService codeEditorService: ICodeEditorService, + @IPathService pathService: IPathService, + @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, + @IUriIdentityService uriIdentityService: IUriIdentityService, + @IModeService modeService: IModeService, + @IElevatedFileService elevatedFileService: IElevatedFileService, + @ILogService logService: ILogService + ) { + super(fileService, untitledTextEditorService, lifecycleService, instantiationService, modelService, environmentService, dialogService, fileDialogService, textResourceConfigurationService, filesConfigurationService, textModelService, codeEditorService, pathService, workingCopyFileService, uriIdentityService, modeService, logService, elevatedFileService); + + this.registerListeners(); + } + + private registerListeners(): void { // Lifecycle this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(), 'veto.textFiles')); diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 274f6075fd..b82b96e676 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -73,14 +73,6 @@ export abstract class AbstractTextFileService extends Disposable implements ITex @IElevatedFileService private readonly elevatedFileService: IElevatedFileService ) { super(); - - this.registerListeners(); - } - - protected registerListeners(): void { - - // Lifecycle - this.lifecycleService.onDidShutdown(() => this.dispose()); } //#region text file read / write / create @@ -465,7 +457,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex // Otherwise try to suggest a path that can be saved let suggestedFilename: string | undefined = undefined; if (resource.scheme === Schemas.untitled) { - const model = this.untitledTextEditorService.get(resource); + const model = this.untitled.get(resource); if (model) { // Untitled with associated file path diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index f0b8834b9a..c7aff2ba0b 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -10,7 +10,7 @@ import { EncodingMode, ITextFileService, TextFileEditorModelState, ITextFileEdit import { IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { IWorkingCopyBackupService, IResolvedWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; -import { IFileService, FileOperationError, FileOperationResult, FileChangesEvent, FileChangeType, IFileStatWithMetadata, ETAG_DISABLED, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; +import { IFileService, FileOperationError, FileOperationResult, FileChangesEvent, FileChangeType, IFileStatWithMetadata, ETAG_DISABLED, FileSystemProviderCapabilities, NotModifiedSinceFileOperationError } from 'vs/platform/files/common/files'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { timeout, TaskSequentializer } from 'vs/base/common/async'; @@ -64,6 +64,9 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private readonly _onDidChangeOrphaned = this._register(new Emitter()); readonly onDidChangeOrphaned = this._onDidChangeOrphaned.event; + private readonly _onDidChangeReadonly = this._register(new Emitter()); + readonly onDidChangeReadonly = this._onDidChangeReadonly.event; + //#endregion readonly typeId = NO_TYPE_ID; // IMPORTANT: never change this to not break existing assumptions (e.g. backups) @@ -91,7 +94,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private inErrorMode = false; constructor( - public readonly resource: URI, + readonly resource: URI, private preferredEncoding: string | undefined, // encoding as chosen by the user private preferredMode: string | undefined, // mode as chosen by the user @IModeService modeService: IModeService, @@ -336,7 +339,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil size, etag, value: buffer, - encoding: preferredEncoding.encoding + encoding: preferredEncoding.encoding, + readonly: false }, true /* dirty (resolved from buffer) */, options); } @@ -382,7 +386,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil size: backup.meta ? backup.meta.size : 0, etag: backup.meta ? backup.meta.etag : ETAG_DISABLED, // etag disabled if unknown! value: await createTextBufferFactoryFromStream(await this.textFileService.getDecodedStream(this.resource, backup.value, { encoding: UTF8 })), - encoding + encoding, + readonly: false }, true /* dirty (resolved from backup) */, options); // Restore orphaned flag based on state @@ -432,8 +437,13 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.setOrphaned(result === FileOperationResult.FILE_NOT_FOUND); // NotModified status is expected and can be handled gracefully - // if we are resolved + // if we are resolved. We still want to update our last resolved + // stat to e.g. detect changes to the file's readonly state if (this.isResolved() && result === FileOperationResult.FILE_NOT_MODIFIED_SINCE) { + if (error instanceof NotModifiedSinceFileOperationError) { + this.updateLastResolvedFileStat(error.stat); + } + return; } @@ -468,6 +478,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil ctime: content.ctime, size: content.size, etag: content.etag, + readonly: content.readonly, isFile: true, isDirectory: false, isSymbolicLink: false @@ -879,6 +890,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private updateLastResolvedFileStat(newFileStat: IFileStatWithMetadata): void { + const oldReadonly = this.isReadonly(); // First resolve - just take if (!this.lastResolvedFileStat) { @@ -891,6 +903,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil else if (this.lastResolvedFileStat.mtime <= newFileStat.mtime) { this.lastResolvedFileStat = newFileStat; } + + // Signal that the readonly state changed + if (this.isReadonly() !== oldReadonly) { + this._onDidChangeReadonly.fire(); + } } //#endregion @@ -996,7 +1013,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } override isReadonly(): boolean { - return this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); + return this.lastResolvedFileStat?.readonly || this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); } override dispose(): void { diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index abd52d207f..ba96a2769f 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -10,10 +10,9 @@ import { URI } from 'vs/base/common/uri'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { dispose, IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ITextFileEditorModel, ITextFileEditorModelManager, ITextFileEditorModelResolveOrCreateOptions, ITextFileResolveEvent, ITextFileSaveEvent, ITextFileSaveParticipant } from 'vs/workbench/services/textfile/common/textfiles'; -import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ResourceMap } from 'vs/base/common/map'; -import { IFileService, FileChangesEvent, FileOperation, FileChangeType } from 'vs/platform/files/common/files'; +import { IFileService, FileChangesEvent, FileOperation, FileChangeType, IFileSystemProviderRegistrationEvent, IFileSystemProviderCapabilitiesChangeEvent } from 'vs/platform/files/common/files'; import { Promises, ResourceQueue } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; import { TextFileSaveParticipant } from 'vs/workbench/services/textfile/common/textFileSaveParticipant'; @@ -72,7 +71,6 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE } constructor( - @ILifecycleService private readonly lifecycleService: ILifecycleService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IFileService private readonly fileService: IFileService, @INotificationService private readonly notificationService: INotificationService, @@ -89,13 +87,14 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE // Update models from file change events this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e))); + // File system provider changes + this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidChangeFileSystemProviderCapabilities(e))); + this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidChangeFileSystemProviderRegistrations(e))); + // Working copy operations this._register(this.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => this.onWillRunWorkingCopyFileOperation(e))); this._register(this.workingCopyFileService.onDidFailWorkingCopyFileOperation(e => this.onDidFailWorkingCopyFileOperation(e))); this._register(this.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => this.onDidRunWorkingCopyFileOperation(e))); - - // Lifecycle - this.lifecycleService.onDidShutdown(() => this.dispose()); } private onDidFilesChange(e: FileChangesEvent): void { @@ -113,6 +112,39 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE } } + private onDidChangeFileSystemProviderCapabilities(e: IFileSystemProviderCapabilitiesChangeEvent): void { + + // Resolve models again for file systems that changed + // capabilities to fetch latest metadata (e.g. readonly) + // into all models. + this.queueModelResolves(e.scheme); + } + + private onDidChangeFileSystemProviderRegistrations(e: IFileSystemProviderRegistrationEvent): void { + if (!e.added) { + return; // only if added + } + + // Resolve models again for file systems that registered + // to account for capability changes: extensions may + // unregister and register the same provider with different + // capabilities, so we want to ensure to fetch latest + // metadata (e.g. readonly) into all models. + this.queueModelResolves(e.scheme); + } + + private queueModelResolves(scheme: string): void { + for (const model of this.models) { + if (model.isDirty() || !model.isResolved()) { + continue; // require a resolved, saved model to continue + } + + if (scheme === model.resource.scheme) { + this.queueModelResolve(model); + } + } + } + private queueModelResolve(model: TextFileEditorModel): void { // Resolve model to update (use a queue to prevent accumulation of resolves @@ -428,21 +460,6 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE //#endregion - clear(): void { - - // model caches - this.mapResourceToModel.clear(); - this.mapResourceToPendingModelResolvers.clear(); - - // dispose the dispose listeners - this.mapResourceToDisposeListener.forEach(listener => listener.dispose()); - this.mapResourceToDisposeListener.clear(); - - // dispose the model change listeners - this.mapResourceToModelListeners.forEach(listener => listener.dispose()); - this.mapResourceToModelListeners.clear(); - } - canDispose(model: TextFileEditorModel): true | Promise { // quick return if model already disposed or not dirty and not resolving @@ -482,6 +499,16 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE override dispose(): void { super.dispose(); - this.clear(); + // model caches + this.mapResourceToModel.clear(); + this.mapResourceToPendingModelResolvers.clear(); + + // dispose the dispose listeners + dispose(this.mapResourceToDisposeListener.values()); + this.mapResourceToDisposeListener.clear(); + + // dispose the model change listeners + dispose(this.mapResourceToModelListeners.values()); + this.mapResourceToModelListeners.clear(); } } diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 9dc9816140..c053e58483 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -476,6 +476,7 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport readonly onDidChangeContent: Event; readonly onDidSaveError: Event; readonly onDidChangeOrphaned: Event; + readonly onDidChangeReadonly: Event; readonly onDidChangeEncoding: Event; hasState(state: TextFileEditorModelState): boolean; diff --git a/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts b/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts index a42873def2..869ef75063 100644 --- a/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts +++ b/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts @@ -54,10 +54,11 @@ export class NativeTextFileService extends AbstractTextFileService { super(fileService, untitledTextEditorService, lifecycleService, instantiationService, modelService, environmentService, dialogService, fileDialogService, textResourceConfigurationService, filesConfigurationService, textModelService, codeEditorService, pathService, workingCopyFileService, uriIdentityService, modeService, logService, elevatedFileService); this.environmentService = environmentService; + + this.registerListeners(); } - protected override registerListeners(): void { - super.registerListeners(); + private registerListeners(): void { // Lifecycle this.lifecycleService.onWillShutdown(event => event.join(this.onWillShutdown(), 'join.textFiles')); diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts new file mode 100644 index 0000000000..b4050e992a --- /dev/null +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts @@ -0,0 +1,800 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; +import { EncodingMode, TextFileEditorModelState, snapshotToString, isTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; +import { createFileEditorInput, workbenchInstantiationService, TestServiceAccessor, TestReadonlyTextFileEditorModel, getLastResolvedFileStat } from 'vs/workbench/test/browser/workbenchTestServices'; +import { toResource } from 'vs/base/test/common/utils'; +import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; +import { FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; +import { timeout } from 'vs/base/common/async'; +import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; +import { assertIsDefined } from 'vs/base/common/types'; +import { createTextBufferFactory, createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { URI } from 'vs/base/common/uri'; +import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; + +suite('Files - TextFileEditorModel', () => { + + function getLastModifiedTime(model: TextFileEditorModel): number { + const stat = getLastResolvedFileStat(model); + + return stat ? stat.mtime : -1; + } + + let instantiationService: IInstantiationService; + let accessor: TestServiceAccessor; + let content: string; + + setup(() => { + instantiationService = workbenchInstantiationService(); + accessor = instantiationService.createInstance(TestServiceAccessor); + content = accessor.fileService.getContent(); + }); + + teardown(() => { + (accessor.textFileService.files).dispose(); + accessor.fileService.setContent(content); + }); + + test('basic events', async function () { + const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + let onDidResolveCounter = 0; + model.onDidResolve(() => onDidResolveCounter++); + + await model.resolve(); + + assert.strictEqual(onDidResolveCounter, 1); + + let onDidChangeContentCounter = 0; + model.onDidChangeContent(() => onDidChangeContentCounter++); + + let onDidChangeDirtyCounter = 0; + model.onDidChangeDirty(() => onDidChangeDirtyCounter++); + + model.updateTextEditorModel(createTextBufferFactory('bar')); + + assert.strictEqual(onDidChangeContentCounter, 1); + assert.strictEqual(onDidChangeDirtyCounter, 1); + + model.updateTextEditorModel(createTextBufferFactory('foo')); + + assert.strictEqual(onDidChangeContentCounter, 2); + assert.strictEqual(onDidChangeDirtyCounter, 1); + + await model.revert(); + + assert.strictEqual(onDidChangeDirtyCounter, 2); + + model.dispose(); + }); + + test('isTextFileEditorModel', async function () { + const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + assert.strictEqual(isTextFileEditorModel(model), true); + + model.dispose(); + }); + + test('save', async function () { + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + await model.resolve(); + + assert.strictEqual(accessor.workingCopyService.dirtyCount, 0); + + let savedEvent = false; + model.onDidSave(() => savedEvent = true); + + await model.save(); + assert.ok(!savedEvent); + + model.updateTextEditorModel(createTextBufferFactory('bar')); + assert.ok(getLastModifiedTime(model) <= Date.now()); + assert.ok(model.hasState(TextFileEditorModelState.DIRTY)); + + assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); + assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), true); + + let workingCopyEvent = false; + accessor.workingCopyService.onDidChangeDirty(e => { + if (e.resource.toString() === model.resource.toString()) { + workingCopyEvent = true; + } + }); + + const pendingSave = model.save(); + assert.ok(model.hasState(TextFileEditorModelState.PENDING_SAVE)); + + await Promise.all([pendingSave, model.joinState(TextFileEditorModelState.PENDING_SAVE)]); + + assert.ok(model.hasState(TextFileEditorModelState.SAVED)); + assert.ok(!model.isDirty()); + assert.ok(savedEvent); + assert.ok(workingCopyEvent); + + assert.strictEqual(accessor.workingCopyService.dirtyCount, 0); + assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), false); + + savedEvent = false; + + await model.save({ force: true }); + assert.ok(savedEvent); + + model.dispose(); + assert.ok(!accessor.modelService.getModel(model.resource)); + }); + + test('save - touching also emits saved event', async function () { + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + await model.resolve(); + + let savedEvent = false; + model.onDidSave(() => savedEvent = true); + + let workingCopyEvent = false; + accessor.workingCopyService.onDidChangeDirty(e => { + if (e.resource.toString() === model.resource.toString()) { + workingCopyEvent = true; + } + }); + + await model.save({ force: true }); + + assert.ok(savedEvent); + assert.ok(!workingCopyEvent); + + model.dispose(); + assert.ok(!accessor.modelService.getModel(model.resource)); + }); + + test('save - touching with error turns model dirty', async function () { + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + await model.resolve(); + + let saveErrorEvent = false; + model.onDidSaveError(() => saveErrorEvent = true); + + let savedEvent = false; + model.onDidSave(() => savedEvent = true); + + accessor.fileService.writeShouldThrowError = new Error('failed to write'); + try { + await model.save({ force: true }); + + assert.ok(model.hasState(TextFileEditorModelState.ERROR)); + assert.ok(model.isDirty()); + assert.ok(saveErrorEvent); + + assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); + assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), true); + } finally { + accessor.fileService.writeShouldThrowError = undefined; + } + + await model.save({ force: true }); + + assert.ok(savedEvent); + assert.strictEqual(model.isDirty(), false); + + model.dispose(); + assert.ok(!accessor.modelService.getModel(model.resource)); + }); + + test('save error (generic)', async function () { + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + await model.resolve(); + + model.updateTextEditorModel(createTextBufferFactory('bar')); + + let saveErrorEvent = false; + model.onDidSaveError(() => saveErrorEvent = true); + + accessor.fileService.writeShouldThrowError = new Error('failed to write'); + try { + const pendingSave = model.save(); + assert.ok(model.hasState(TextFileEditorModelState.PENDING_SAVE)); + + await pendingSave; + + assert.ok(model.hasState(TextFileEditorModelState.ERROR)); + assert.ok(model.isDirty()); + assert.ok(saveErrorEvent); + + assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); + assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), true); + + model.dispose(); + } finally { + accessor.fileService.writeShouldThrowError = undefined; + } + }); + + test('save error (conflict)', async function () { + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + await model.resolve(); + + model.updateTextEditorModel(createTextBufferFactory('bar')); + + let saveErrorEvent = false; + model.onDidSaveError(() => saveErrorEvent = true); + + accessor.fileService.writeShouldThrowError = new FileOperationError('save conflict', FileOperationResult.FILE_MODIFIED_SINCE); + try { + const pendingSave = model.save(); + assert.ok(model.hasState(TextFileEditorModelState.PENDING_SAVE)); + + await pendingSave; + + assert.ok(model.hasState(TextFileEditorModelState.CONFLICT)); + assert.ok(model.isDirty()); + assert.ok(saveErrorEvent); + + assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); + assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), true); + + model.dispose(); + } finally { + accessor.fileService.writeShouldThrowError = undefined; + } + }); + + test('setEncoding - encode', async function () { + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + let encodingEvent = false; + model.onDidChangeEncoding(() => encodingEvent = true); + + await model.setEncoding('utf8', EncodingMode.Encode); // no-op + assert.strictEqual(getLastModifiedTime(model), -1); + + assert.ok(!encodingEvent); + + await model.setEncoding('utf16', EncodingMode.Encode); + + assert.ok(encodingEvent); + + assert.ok(getLastModifiedTime(model) <= Date.now()); // indicates model was saved due to encoding change + + model.dispose(); + }); + + test('setEncoding - decode', async function () { + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + await model.setEncoding('utf16', EncodingMode.Decode); + + assert.ok(model.isResolved()); // model got resolved due to decoding + model.dispose(); + }); + + test('setEncoding - decode dirty file saves first', async function () { + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + await model.resolve(); + + model.updateTextEditorModel(createTextBufferFactory('bar')); + assert.strictEqual(model.isDirty(), true); + + await model.setEncoding('utf16', EncodingMode.Decode); + + assert.strictEqual(model.isDirty(), false); + model.dispose(); + }); + + test('create with mode', async function () { + const mode = 'text-file-model-test'; + ModesRegistry.registerLanguage({ + id: mode, + }); + + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', mode); + + await model.resolve(); + + assert.strictEqual(model.textEditorModel!.getModeId(), mode); + + model.dispose(); + assert.ok(!accessor.modelService.getModel(model.resource)); + }); + + test('disposes when underlying model is destroyed', async function () { + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + await model.resolve(); + + model.textEditorModel!.dispose(); + assert.ok(model.isDisposed()); + }); + + test('Resolve does not trigger save', async function () { + const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index.txt'), 'utf8', undefined); + assert.ok(model.hasState(TextFileEditorModelState.SAVED)); + + model.onDidSave(() => assert.fail()); + model.onDidChangeDirty(() => assert.fail()); + + await model.resolve(); + assert.ok(model.isResolved()); + model.dispose(); + assert.ok(!accessor.modelService.getModel(model.resource)); + }); + + test('Resolve returns dirty model as long as model is dirty', async function () { + const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + await model.resolve(); + model.updateTextEditorModel(createTextBufferFactory('foo')); + assert.ok(model.isDirty()); + assert.ok(model.hasState(TextFileEditorModelState.DIRTY)); + + await model.resolve(); + assert.ok(model.isDirty()); + model.dispose(); + }); + + test('Resolve with contents', async function () { + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + await model.resolve({ contents: createTextBufferFactory('Hello World') }); + + assert.strictEqual(model.textEditorModel?.getValue(), 'Hello World'); + assert.strictEqual(model.isDirty(), true); + + await model.resolve({ contents: createTextBufferFactory('Hello Changes') }); + + assert.strictEqual(model.textEditorModel?.getValue(), 'Hello Changes'); + assert.strictEqual(model.isDirty(), true); + + // verify that we do not mark the model as saved when undoing once because + // we never really had a saved state + await model.textEditorModel!.undo(); + assert.ok(model.isDirty()); + + model.dispose(); + assert.ok(!accessor.modelService.getModel(model.resource)); + }); + + test('Revert', async function () { + let eventCounter = 0; + + const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + model.onDidRevert(() => eventCounter++); + + let workingCopyEvent = false; + accessor.workingCopyService.onDidChangeDirty(e => { + if (e.resource.toString() === model.resource.toString()) { + workingCopyEvent = true; + } + }); + + await model.resolve(); + model.updateTextEditorModel(createTextBufferFactory('foo')); + assert.ok(model.isDirty()); + + assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); + assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), true); + + await model.revert(); + assert.strictEqual(model.isDirty(), false); + assert.strictEqual(model.textEditorModel!.getValue(), 'Hello Html'); + assert.strictEqual(eventCounter, 1); + + assert.ok(workingCopyEvent); + assert.strictEqual(accessor.workingCopyService.dirtyCount, 0); + assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), false); + + model.dispose(); + }); + + test('Revert (soft)', async function () { + let eventCounter = 0; + + const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + model.onDidRevert(() => eventCounter++); + + let workingCopyEvent = false; + accessor.workingCopyService.onDidChangeDirty(e => { + if (e.resource.toString() === model.resource.toString()) { + workingCopyEvent = true; + } + }); + + await model.resolve(); + model.updateTextEditorModel(createTextBufferFactory('foo')); + assert.ok(model.isDirty()); + + assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); + assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), true); + + await model.revert({ soft: true }); + assert.strictEqual(model.isDirty(), false); + assert.strictEqual(model.textEditorModel!.getValue(), 'foo'); + assert.strictEqual(eventCounter, 1); + + assert.ok(workingCopyEvent); + assert.strictEqual(accessor.workingCopyService.dirtyCount, 0); + assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), false); + + model.dispose(); + }); + + test('Undo to saved state turns model non-dirty', async function () { + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + await model.resolve(); + model.updateTextEditorModel(createTextBufferFactory('Hello Text')); + assert.ok(model.isDirty()); + + await model.textEditorModel!.undo(); + assert.ok(!model.isDirty()); + }); + + test('Resolve and undo turns model dirty', async function () { + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + await model.resolve(); + accessor.fileService.setContent('Hello Change'); + + await model.resolve(); + await model.textEditorModel!.undo(); + assert.ok(model.isDirty()); + + assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); + assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), true); + }); + + test('Update Dirty', async function () { + let eventCounter = 0; + + const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + model.setDirty(true); + assert.ok(!model.isDirty()); // needs to be resolved + + await model.resolve(); + model.updateTextEditorModel(createTextBufferFactory('foo')); + assert.ok(model.isDirty()); + + await model.revert({ soft: true }); + assert.strictEqual(model.isDirty(), false); + + model.onDidChangeDirty(() => eventCounter++); + + let workingCopyEvent = false; + accessor.workingCopyService.onDidChangeDirty(e => { + if (e.resource.toString() === model.resource.toString()) { + workingCopyEvent = true; + } + }); + + model.setDirty(true); + assert.ok(model.isDirty()); + assert.strictEqual(eventCounter, 1); + assert.ok(workingCopyEvent); + + model.setDirty(false); + assert.strictEqual(model.isDirty(), false); + assert.strictEqual(eventCounter, 2); + + model.dispose(); + }); + + test('No Dirty or saving for readonly models', async function () { + let workingCopyEvent = false; + accessor.workingCopyService.onDidChangeDirty(e => { + if (e.resource.toString() === model.resource.toString()) { + workingCopyEvent = true; + } + }); + + const model = instantiationService.createInstance(TestReadonlyTextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + let saveEvent = false; + model.onDidSave(() => { + saveEvent = true; + }); + + await model.resolve(); + model.updateTextEditorModel(createTextBufferFactory('foo')); + assert.ok(!model.isDirty()); + + await model.save({ force: true }); + assert.strictEqual(saveEvent, false); + + await model.revert({ soft: true }); + assert.ok(!model.isDirty()); + + assert.ok(!workingCopyEvent); + + model.dispose(); + }); + + test('File not modified error is handled gracefully', async function () { + let model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + await model.resolve(); + + const mtime = getLastModifiedTime(model); + accessor.textFileService.setReadStreamErrorOnce(new FileOperationError('error', FileOperationResult.FILE_NOT_MODIFIED_SINCE)); + + await model.resolve(); + + assert.ok(model); + assert.strictEqual(getLastModifiedTime(model), mtime); + model.dispose(); + }); + + test('Resolve error is handled gracefully if model already exists', async function () { + let model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + await model.resolve(); + accessor.textFileService.setReadStreamErrorOnce(new FileOperationError('error', FileOperationResult.FILE_NOT_FOUND)); + + await model.resolve(); + assert.ok(model); + model.dispose(); + }); + + test('save() and isDirty() - proper with check for mtimes', async function () { + const input1 = createFileEditorInput(instantiationService, toResource.call(this, '/path/index_async2.txt')); + const input2 = createFileEditorInput(instantiationService, toResource.call(this, '/path/index_async.txt')); + + const model1 = await input1.resolve() as TextFileEditorModel; + const model2 = await input2.resolve() as TextFileEditorModel; + + model1.updateTextEditorModel(createTextBufferFactory('foo')); + + const m1Mtime = assertIsDefined(getLastResolvedFileStat(model1)).mtime; + const m2Mtime = assertIsDefined(getLastResolvedFileStat(model2)).mtime; + assert.ok(m1Mtime > 0); + assert.ok(m2Mtime > 0); + + assert.ok(accessor.textFileService.isDirty(toResource.call(this, '/path/index_async2.txt'))); + assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt'))); + + model2.updateTextEditorModel(createTextBufferFactory('foo')); + assert.ok(accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt'))); + + await timeout(10); + await accessor.textFileService.save(toResource.call(this, '/path/index_async.txt')); + await accessor.textFileService.save(toResource.call(this, '/path/index_async2.txt')); + assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt'))); + assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async2.txt'))); + assert.ok(assertIsDefined(getLastResolvedFileStat(model1)).mtime > m1Mtime); + assert.ok(assertIsDefined(getLastResolvedFileStat(model2)).mtime > m2Mtime); + + model1.dispose(); + model2.dispose(); + }); + + test('Save Participant', async function () { + let eventCounter = 0; + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + model.onDidSave(() => { + assert.strictEqual(snapshotToString(model.createSnapshot()!), eventCounter === 1 ? 'bar' : 'foobar'); + assert.ok(!model.isDirty()); + eventCounter++; + }); + + const participant = accessor.textFileService.files.addSaveParticipant({ + participate: async model => { + assert.ok(model.isDirty()); + (model as TextFileEditorModel).updateTextEditorModel(createTextBufferFactory('bar')); + assert.ok(model.isDirty()); + eventCounter++; + } + }); + + await model.resolve(); + model.updateTextEditorModel(createTextBufferFactory('foo')); + assert.ok(model.isDirty()); + + await model.save(); + assert.strictEqual(eventCounter, 2); + + participant.dispose(); + model.updateTextEditorModel(createTextBufferFactory('foobar')); + assert.ok(model.isDirty()); + + await model.save(); + assert.strictEqual(eventCounter, 3); + + model.dispose(); + }); + + test('Save Participant - skip', async function () { + let eventCounter = 0; + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + const participant = accessor.textFileService.files.addSaveParticipant({ + participate: async () => { + eventCounter++; + } + }); + + await model.resolve(); + model.updateTextEditorModel(createTextBufferFactory('foo')); + + await model.save({ skipSaveParticipants: true }); + assert.strictEqual(eventCounter, 0); + + participant.dispose(); + model.dispose(); + }); + + test('Save Participant, async participant', async function () { + let eventCounter = 0; + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + model.onDidSave(() => { + assert.ok(!model.isDirty()); + eventCounter++; + }); + + const participant = accessor.textFileService.files.addSaveParticipant({ + participate: model => { + assert.ok(model.isDirty()); + (model as TextFileEditorModel).updateTextEditorModel(createTextBufferFactory('bar')); + assert.ok(model.isDirty()); + eventCounter++; + + return timeout(10); + } + }); + + await model.resolve(); + model.updateTextEditorModel(createTextBufferFactory('foo')); + + const now = Date.now(); + await model.save(); + assert.strictEqual(eventCounter, 2); + assert.ok(Date.now() - now >= 10); + + model.dispose(); + participant.dispose(); + }); + + test('Save Participant, bad participant', async function () { + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + const participant = accessor.textFileService.files.addSaveParticipant({ + participate: async () => { + new Error('boom'); + } + }); + + await model.resolve(); + model.updateTextEditorModel(createTextBufferFactory('foo')); + + await model.save(); + + model.dispose(); + participant.dispose(); + }); + + test('Save Participant, participant cancelled when saved again', async function () { + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + let participations: boolean[] = []; + + const participant = accessor.textFileService.files.addSaveParticipant({ + participate: async (model, context, progress, token) => { + await timeout(10); + + if (!token.isCancellationRequested) { + participations.push(true); + } + } + }); + + await model.resolve(); + + model.updateTextEditorModel(createTextBufferFactory('foo')); + const p1 = model.save(); + + model.updateTextEditorModel(createTextBufferFactory('foo 1')); + const p2 = model.save(); + + model.updateTextEditorModel(createTextBufferFactory('foo 2')); + const p3 = model.save(); + + model.updateTextEditorModel(createTextBufferFactory('foo 3')); + const p4 = model.save(); + + await Promise.all([p1, p2, p3, p4]); + assert.strictEqual(participations.length, 1); + + model.dispose(); + participant.dispose(); + }); + + test('Save Participant, calling save from within is unsupported but does not explode (sync save)', async function () { + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + await testSaveFromSaveParticipant(model, false); + + model.dispose(); + }); + + test('Save Participant, calling save from within is unsupported but does not explode (async save)', async function () { + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + await testSaveFromSaveParticipant(model, true); + + model.dispose(); + }); + + async function testSaveFromSaveParticipant(model: TextFileEditorModel, async: boolean): Promise { + let savePromise: Promise; + let breakLoop = false; + + const participant = accessor.textFileService.files.addSaveParticipant({ + participate: async model => { + if (breakLoop) { + return; + } + + breakLoop = true; + + if (async) { + await timeout(10); + } + const newSavePromise = model.save(); + + // assert that this is the same promise as the outer one + assert.strictEqual(savePromise, newSavePromise); + } + }); + + await model.resolve(); + model.updateTextEditorModel(createTextBufferFactory('foo')); + + savePromise = model.save(); + await savePromise; + + participant.dispose(); + } + + test('backup and restore (simple)', async function () { + return testBackupAndRestore(toResource.call(this, '/path/index_async.txt'), toResource.call(this, '/path/index_async2.txt'), 'Some very small file text content.'); + }); + + test('backup and restore (large, #121347)', async function () { + const largeContent = '국어한\n'.repeat(100000); + return testBackupAndRestore(toResource.call(this, '/path/index_async.txt'), toResource.call(this, '/path/index_async2.txt'), largeContent); + }); + + async function testBackupAndRestore(resourceA: URI, resourceB: URI, contents: string): Promise { + const originalModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, resourceA, 'utf8', undefined); + await originalModel.resolve({ + contents: await createTextBufferFactoryFromStream(await accessor.textFileService.getDecodedStream(resourceA, bufferToStream(VSBuffer.fromString(contents)))) + }); + + assert.strictEqual(originalModel.textEditorModel?.getValue(), contents); + + const backup = await originalModel.backup(CancellationToken.None); + const modelRestoredIdentifier = { typeId: originalModel.typeId, resource: resourceB }; + await accessor.workingCopyBackupService.backup(modelRestoredIdentifier, backup.content); + + const modelRestored: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, modelRestoredIdentifier.resource, 'utf8', undefined); + await modelRestored.resolve(); + + assert.strictEqual(modelRestored.textEditorModel?.getValue(), contents); + assert.strictEqual(modelRestored.isDirty(), true); + + originalModel.dispose(); + modelRestored.dispose(); + } +}); diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts new file mode 100644 index 0000000000..a64df3591d --- /dev/null +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts @@ -0,0 +1,324 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; +import { workbenchInstantiationService, TestServiceAccessor, TestTextFileEditorModelManager } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; +import { FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files'; +import { toResource } from 'vs/base/test/common/utils'; +import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; +import { ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; +import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; +import { timeout } from 'vs/base/common/async'; + +suite('Files - TextFileEditorModelManager', () => { + + let instantiationService: IInstantiationService; + let accessor: TestServiceAccessor; + + setup(() => { + instantiationService = workbenchInstantiationService(); + accessor = instantiationService.createInstance(TestServiceAccessor); + }); + + test('add, remove, clear, get, getAll', function () { + const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + + const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random1.txt'), 'utf8', undefined); + const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random2.txt'), 'utf8', undefined); + const model3: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random3.txt'), 'utf8', undefined); + + manager.add(URI.file('/test.html'), model1); + manager.add(URI.file('/some/other.html'), model2); + manager.add(URI.file('/some/this.txt'), model3); + + const fileUpper = URI.file('/TEST.html'); + + assert(!manager.get(URI.file('foo'))); + assert.strictEqual(manager.get(URI.file('/test.html')), model1); + + assert.ok(!manager.get(fileUpper)); + + let results = manager.models; + assert.strictEqual(3, results.length); + + let result = manager.get(URI.file('/yes')); + assert.ok(!result); + + result = manager.get(URI.file('/some/other.txt')); + assert.ok(!result); + + result = manager.get(URI.file('/some/other.html')); + assert.ok(result); + + result = manager.get(fileUpper); + assert.ok(!result); + + manager.remove(URI.file('')); + + results = manager.models; + assert.strictEqual(3, results.length); + + manager.remove(URI.file('/some/other.html')); + results = manager.models; + assert.strictEqual(2, results.length); + + manager.remove(fileUpper); + results = manager.models; + assert.strictEqual(2, results.length); + + manager.dispose(); + results = manager.models; + assert.strictEqual(0, results.length); + + model1.dispose(); + model2.dispose(); + model3.dispose(); + + manager.dispose(); + }); + + test('resolve', async () => { + const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + const resource = URI.file('/test.html'); + const encoding = 'utf8'; + + const events: ITextFileEditorModel[] = []; + const listener = manager.onDidCreate(model => { + events.push(model); + }); + + const modelPromise = manager.resolve(resource, { encoding }); + assert.ok(manager.get(resource)); // model known even before resolved() + + const model1 = await modelPromise; + assert.ok(model1); + assert.strictEqual(model1.getEncoding(), encoding); + assert.strictEqual(manager.get(resource), model1); + + const model2 = await manager.resolve(resource, { encoding }); + assert.strictEqual(model2, model1); + model1.dispose(); + + const model3 = await manager.resolve(resource, { encoding }); + assert.notStrictEqual(model3, model2); + assert.strictEqual(manager.get(resource), model3); + model3.dispose(); + + assert.strictEqual(events.length, 2); + assert.strictEqual(events[0].resource.toString(), model1.resource.toString()); + assert.strictEqual(events[1].resource.toString(), model2.resource.toString()); + + listener.dispose(); + + model1.dispose(); + model2.dispose(); + model3.dispose(); + + manager.dispose(); + }); + + test('resolve with initial contents', async () => { + const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + const resource = URI.file('/test.html'); + + const model = await manager.resolve(resource, { contents: createTextBufferFactory('Hello World') }); + assert.strictEqual(model.textEditorModel?.getValue(), 'Hello World'); + assert.strictEqual(model.isDirty(), true); + + await manager.resolve(resource, { contents: createTextBufferFactory('More Changes') }); + assert.strictEqual(model.textEditorModel?.getValue(), 'More Changes'); + assert.strictEqual(model.isDirty(), true); + + model.dispose(); + manager.dispose(); + }); + + test('multiple resolves execute in sequence', async () => { + const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + const resource = URI.file('/test.html'); + + const firstModelPromise = manager.resolve(resource); + const secondModelPromise = manager.resolve(resource, { contents: createTextBufferFactory('Hello World') }); + const thirdModelPromise = manager.resolve(resource, { contents: createTextBufferFactory('More Changes') }); + + await firstModelPromise; + await secondModelPromise; + const model = await thirdModelPromise; + + assert.strictEqual(model.textEditorModel?.getValue(), 'More Changes'); + assert.strictEqual(model.isDirty(), true); + + model.dispose(); + manager.dispose(); + }); + + test('removed from cache when model disposed', function () { + const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + + const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random1.txt'), 'utf8', undefined); + const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random2.txt'), 'utf8', undefined); + const model3: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random3.txt'), 'utf8', undefined); + + manager.add(URI.file('/test.html'), model1); + manager.add(URI.file('/some/other.html'), model2); + manager.add(URI.file('/some/this.txt'), model3); + + assert.strictEqual(manager.get(URI.file('/test.html')), model1); + + model1.dispose(); + assert(!manager.get(URI.file('/test.html'))); + + model2.dispose(); + model3.dispose(); + + manager.dispose(); + }); + + test('events', async function () { + const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager); + + const resource1 = toResource.call(this, '/path/index.txt'); + const resource2 = toResource.call(this, '/path/other.txt'); + + let resolvedCounter = 0; + let gotDirtyCounter = 0; + let gotNonDirtyCounter = 0; + let revertedCounter = 0; + let savedCounter = 0; + let encodingCounter = 0; + + manager.onDidResolve(({ model }) => { + if (model.resource.toString() === resource1.toString()) { + resolvedCounter++; + } + }); + + manager.onDidChangeDirty(model => { + if (model.resource.toString() === resource1.toString()) { + if (model.isDirty()) { + gotDirtyCounter++; + } else { + gotNonDirtyCounter++; + } + } + }); + + manager.onDidRevert(model => { + if (model.resource.toString() === resource1.toString()) { + revertedCounter++; + } + }); + + manager.onDidSave(({ model }) => { + if (model.resource.toString() === resource1.toString()) { + savedCounter++; + } + }); + + manager.onDidChangeEncoding(model => { + if (model.resource.toString() === resource1.toString()) { + encodingCounter++; + } + }); + + const model1 = await manager.resolve(resource1, { encoding: 'utf8' }); + assert.strictEqual(resolvedCounter, 1); + + accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.DELETED }], false)); + accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.ADDED }], false)); + + const model2 = await manager.resolve(resource2, { encoding: 'utf8' }); + assert.strictEqual(resolvedCounter, 2); + + model1.updateTextEditorModel(createTextBufferFactory('changed')); + model1.updatePreferredEncoding('utf16'); + + await model1.revert(); + model1.updateTextEditorModel(createTextBufferFactory('changed again')); + + await model1.save(); + model1.dispose(); + model2.dispose(); + + await model1.revert(); + assert.strictEqual(gotDirtyCounter, 2); + assert.strictEqual(gotNonDirtyCounter, 2); + assert.strictEqual(revertedCounter, 1); + assert.strictEqual(savedCounter, 1); + assert.strictEqual(encodingCounter, 2); + + model1.dispose(); + model2.dispose(); + assert.ok(!accessor.modelService.getModel(resource1)); + assert.ok(!accessor.modelService.getModel(resource2)); + + manager.dispose(); + }); + + test('disposing model takes it out of the manager', async function () { + const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager); + + const resource = toResource.call(this, '/path/index_something.txt'); + + const model = await manager.resolve(resource, { encoding: 'utf8' }); + model.dispose(); + assert.ok(!manager.get(resource)); + assert.ok(!accessor.modelService.getModel(model.resource)); + manager.dispose(); + }); + + test('canDispose with dirty model', async function () { + const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager); + + const resource = toResource.call(this, '/path/index_something.txt'); + + const model = await manager.resolve(resource, { encoding: 'utf8' }); + model.updateTextEditorModel(createTextBufferFactory('make dirty')); + + let canDisposePromise = manager.canDispose(model as TextFileEditorModel); + assert.ok(canDisposePromise instanceof Promise); + + let canDispose = false; + (async () => { + canDispose = await canDisposePromise; + })(); + + assert.strictEqual(canDispose, false); + model.revert({ soft: true }); + + await timeout(0); + + assert.strictEqual(canDispose, true); + + let canDispose2 = manager.canDispose(model as TextFileEditorModel); + assert.strictEqual(canDispose2, true); + + manager.dispose(); + }); + + test('mode', async function () { + const mode = 'text-file-model-manager-test'; + ModesRegistry.registerLanguage({ + id: mode, + }); + + const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager); + + const resource = toResource.call(this, '/path/index_something.txt'); + + let model = await manager.resolve(resource, { mode }); + assert.strictEqual(model.textEditorModel!.getModeId(), mode); + + model = await manager.resolve(resource, { mode: 'text' }); + assert.strictEqual(model.textEditorModel!.getModeId(), PLAINTEXT_MODE_ID); + + model.dispose(); + manager.dispose(); + }); +}); diff --git a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts new file mode 100644 index 0000000000..f4378500d7 --- /dev/null +++ b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { workbenchInstantiationService, TestServiceAccessor, TestTextFileEditorModelManager } from 'vs/workbench/test/browser/workbenchTestServices'; +import { toResource } from 'vs/base/test/common/utils'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; +import { FileOperation } from 'vs/platform/files/common/files'; +import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; + +suite('Files - TextFileService', () => { + + let instantiationService: IInstantiationService; + let model: TextFileEditorModel; + let accessor: TestServiceAccessor; + + setup(() => { + instantiationService = workbenchInstantiationService(); + accessor = instantiationService.createInstance(TestServiceAccessor); + }); + + teardown(() => { + model?.dispose(); + (accessor.textFileService.files).dispose(); + }); + + test('isDirty/getDirty - files and untitled', async function () { + model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); + (accessor.textFileService.files).add(model.resource, model); + + await model.resolve(); + + assert.ok(!accessor.textFileService.isDirty(model.resource)); + model.textEditorModel!.setValue('foo'); + + assert.ok(accessor.textFileService.isDirty(model.resource)); + + const untitled = await accessor.textFileService.untitled.resolve(); + + assert.ok(!accessor.textFileService.isDirty(untitled.resource)); + untitled.textEditorModel?.setValue('changed'); + + assert.ok(accessor.textFileService.isDirty(untitled.resource)); + + untitled.dispose(); + model.dispose(); + }); + + test('save - file', async function () { + model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); + (accessor.textFileService.files).add(model.resource, model); + + await model.resolve(); + model.textEditorModel!.setValue('foo'); + assert.ok(accessor.textFileService.isDirty(model.resource)); + + const res = await accessor.textFileService.save(model.resource); + assert.strictEqual(res?.toString(), model.resource.toString()); + assert.ok(!accessor.textFileService.isDirty(model.resource)); + }); + + test('saveAll - file', async function () { + model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); + (accessor.textFileService.files).add(model.resource, model); + + await model.resolve(); + model.textEditorModel!.setValue('foo'); + assert.ok(accessor.textFileService.isDirty(model.resource)); + + const res = await accessor.textFileService.save(model.resource); + assert.strictEqual(res?.toString(), model.resource.toString()); + assert.ok(!accessor.textFileService.isDirty(model.resource)); + }); + + test('saveAs - file', async function () { + model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); + (accessor.textFileService.files).add(model.resource, model); + accessor.fileDialogService.setPickFileToSave(model.resource); + + await model.resolve(); + model.textEditorModel!.setValue('foo'); + assert.ok(accessor.textFileService.isDirty(model.resource)); + + const res = await accessor.textFileService.saveAs(model.resource); + assert.strictEqual(res!.toString(), model.resource.toString()); + assert.ok(!accessor.textFileService.isDirty(model.resource)); + }); + + test('revert - file', async function () { + model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); + (accessor.textFileService.files).add(model.resource, model); + accessor.fileDialogService.setPickFileToSave(model.resource); + + await model.resolve(); + model!.textEditorModel!.setValue('foo'); + assert.ok(accessor.textFileService.isDirty(model.resource)); + + await accessor.textFileService.revert(model.resource); + assert.ok(!accessor.textFileService.isDirty(model.resource)); + }); + + test('create does not overwrite existing model', async function () { + model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); + (accessor.textFileService.files).add(model.resource, model); + + await model.resolve(); + model!.textEditorModel!.setValue('foo'); + assert.ok(accessor.textFileService.isDirty(model.resource)); + + let eventCounter = 0; + + const disposable1 = accessor.workingCopyFileService.addFileOperationParticipant({ + participate: async files => { + assert.strictEqual(files[0].target.toString(), model.resource.toString()); + eventCounter++; + } + }); + + const disposable2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => { + assert.strictEqual(e.operation, FileOperation.CREATE); + assert.strictEqual(e.files[0].target.toString(), model.resource.toString()); + eventCounter++; + }); + + await accessor.textFileService.create([{ resource: model.resource, value: 'Foo' }]); + assert.ok(!accessor.textFileService.isDirty(model.resource)); + + assert.strictEqual(eventCounter, 2); + + disposable1.dispose(); + disposable2.dispose(); + }); + + test('Filename Suggestion - Suggest prefix only when there are no relevant extensions', () => { + ModesRegistry.registerLanguage({ + id: 'plumbus0', + extensions: ['.one', '.two'] + }); + + let suggested = accessor.textFileService.suggestFilename('shleem', 'Untitled-1'); + assert.strictEqual(suggested, 'Untitled-1'); + }); + + test('Filename Suggestion - Suggest prefix with first extension', () => { + ModesRegistry.registerLanguage({ + id: 'plumbus1', + extensions: ['.shleem', '.gazorpazorp'], + filenames: ['plumbus'] + }); + + let suggested = accessor.textFileService.suggestFilename('plumbus1', 'Untitled-1'); + assert.strictEqual(suggested, 'Untitled-1.shleem'); + }); + + test('Filename Suggestion - Suggest filename if there are no extensions', () => { + ModesRegistry.registerLanguage({ + id: 'plumbus2', + filenames: ['plumbus', 'shleem', 'gazorpazorp'] + }); + + let suggested = accessor.textFileService.suggestFilename('plumbus2', 'Untitled-1'); + assert.strictEqual(suggested, 'plumbus'); + }); +}); diff --git a/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.io.test.ts b/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.io.test.ts index 39d7c71b7e..b03548daa5 100644 --- a/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.io.test.ts +++ b/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.io.test.ts @@ -4,13 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { tmpdir } from 'os'; -import { promises } from 'fs'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IFileService } from 'vs/platform/files/common/files'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { Schemas } from 'vs/base/common/network'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { rimraf, copy, exists } from 'vs/base/node/pfs'; +import { Promises } from 'vs/base/node/pfs'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { FileService } from 'vs/platform/files/common/fileService'; import { NullLogService } from 'vs/platform/log/common/log'; @@ -32,7 +31,7 @@ flakySuite('Files - NativeTextFileService i/o', function () { function readFile(path: string): Promise; function readFile(path: string, encoding: BufferEncoding): Promise; function readFile(path: string, encoding?: BufferEncoding): Promise { - return promises.readFile(path, encoding); + return Promises.readFile(path, encoding); } createSuite({ @@ -56,7 +55,7 @@ flakySuite('Files - NativeTextFileService i/o', function () { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'textfileservice'); const sourceDir = getPathFromAmdModule(require, './fixtures'); - await copy(sourceDir, testDir, { preserveSymlinks: false }); + await Promises.copy(sourceDir, testDir, { preserveSymlinks: false }); return { service, testDir }; }, @@ -66,11 +65,11 @@ flakySuite('Files - NativeTextFileService i/o', function () { disposables.clear(); - return rimraf(testDir); + return Promises.rm(testDir); }, - exists, - stat: promises.stat, + exists: Promises.exists, + stat: Promises.stat, readFile, detectEncodingByBOM }); diff --git a/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.test.ts b/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.test.ts index f822ddd458..c4a8125cd0 100644 --- a/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.test.ts +++ b/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.test.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; diff --git a/src/vs/workbench/services/textfile/test/node/encoding/fixtures/some_ansi.css b/src/vs/workbench/services/textfile/test/node/encoding/fixtures/some_ansi.css index b7e5283202..c5cea74684 100644 --- a/src/vs/workbench/services/textfile/test/node/encoding/fixtures/some_ansi.css +++ b/src/vs/workbench/services/textfile/test/node/encoding/fixtures/some_ansi.css @@ -25,12 +25,12 @@ h1, h2, h3, h4, h5, h6 margin: 0px; } -textarea +textarea { font-family: Consolas } -#results +#results { margin-top: 2em; margin-left: 2em; diff --git a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts index 0eb1c0f09f..7c9356fd6a 100644 --- a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts +++ b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts @@ -6,9 +6,9 @@ import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITextModel } from 'vs/editor/common/model'; -import { IDisposable, toDisposable, IReference, ReferenceCollection, Disposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable, IReference, ReferenceCollection, Disposable, AsyncReferenceCollection } from 'vs/base/common/lifecycle'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; +import { TextResourceEditorModel } from 'vs/workbench/common/editor/textResourceEditorModel'; import { ITextFileService, TextFileResolveReason } from 'vs/workbench/services/textfile/common/textfiles'; import { Schemas } from 'vs/base/common/network'; import { ITextModelService, ITextModelContentProvider, ITextEditorModel, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; @@ -50,7 +50,7 @@ class ResourceModelCollection extends ReferenceCollection ref.dispose() - }; - } catch (error) { - ref.dispose(); - - throw error; - } + const result = await this.asyncModelCollection.acquire(resource.toString()); + return result as IReference; // TODO@Ben: why is this cast here? } registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable { diff --git a/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts b/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts new file mode 100644 index 0000000000..cc0e5f7e6c --- /dev/null +++ b/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts @@ -0,0 +1,210 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ITextModel } from 'vs/editor/common/model'; +import { URI } from 'vs/base/common/uri'; +import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; +import { TextResourceEditorModel } from 'vs/workbench/common/editor/textResourceEditorModel'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { workbenchInstantiationService, TestServiceAccessor, TestTextFileEditorModelManager } from 'vs/workbench/test/browser/workbenchTestServices'; +import { toResource } from 'vs/base/test/common/utils'; +import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; +import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; +import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; +import { Event } from 'vs/base/common/event'; +import { timeout } from 'vs/base/common/async'; +import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; +import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; + +suite('Workbench - TextModelResolverService', () => { + + let instantiationService: IInstantiationService; + let accessor: TestServiceAccessor; + let model: TextFileEditorModel; + + setup(() => { + instantiationService = workbenchInstantiationService(); + accessor = instantiationService.createInstance(TestServiceAccessor); + }); + + teardown(() => { + model?.dispose(); + (accessor.textFileService.files).dispose(); + }); + + test('resolve resource', async () => { + const disposable = accessor.textModelResolverService.registerTextModelContentProvider('test', { + provideTextContent: async function (resource: URI): Promise { + if (resource.scheme === 'test') { + let modelContent = 'Hello Test'; + let languageSelection = accessor.modeService.create('json'); + + return accessor.modelService.createModel(modelContent, languageSelection, resource); + } + + return null; + } + }); + + let resource = URI.from({ scheme: 'test', authority: null!, path: 'thePath' }); + let input = instantiationService.createInstance(TextResourceEditorInput, resource, 'The Name', 'The Description', undefined, undefined); + + const model = await input.resolve(); + assert.ok(model); + assert.strictEqual(snapshotToString(((model as TextResourceEditorModel).createSnapshot()!)), 'Hello Test'); + let disposed = false; + let disposedPromise = new Promise(resolve => { + Event.once(model.onWillDispose)(() => { + disposed = true; + resolve(); + }); + }); + input.dispose(); + + await disposedPromise; + assert.strictEqual(disposed, true); + disposable.dispose(); + }); + + test('resolve file', async function () { + const textModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_resolver.txt'), 'utf8', undefined); + (accessor.textFileService.files).add(textModel.resource, textModel); + + await textModel.resolve(); + + const ref = await accessor.textModelResolverService.createModelReference(textModel.resource); + + const model = ref.object; + const editorModel = model.textEditorModel; + + assert.ok(editorModel); + assert.strictEqual(editorModel.getValue(), 'Hello Html'); + + let disposed = false; + Event.once(model.onWillDispose)(() => { + disposed = true; + }); + + ref.dispose(); + await timeout(0); // due to the reference resolving the model first which is async + assert.strictEqual(disposed, true); + }); + + test('resolved dirty file eventually disposes', async function () { + const textModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_resolver.txt'), 'utf8', undefined); + (accessor.textFileService.files).add(textModel.resource, textModel); + + await textModel.resolve(); + + textModel.updateTextEditorModel(createTextBufferFactory('make dirty')); + + const ref = await accessor.textModelResolverService.createModelReference(textModel.resource); + + let disposed = false; + Event.once(textModel.onWillDispose)(() => { + disposed = true; + }); + + ref.dispose(); + await timeout(0); + assert.strictEqual(disposed, false); // not disposed because model still dirty + + textModel.revert(); + + await timeout(0); + assert.strictEqual(disposed, true); // now disposed because model got reverted + }); + + test('resolved dirty file does not dispose when new reference created', async function () { + const textModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_resolver.txt'), 'utf8', undefined); + (accessor.textFileService.files).add(textModel.resource, textModel); + + await textModel.resolve(); + + textModel.updateTextEditorModel(createTextBufferFactory('make dirty')); + + const ref1 = await accessor.textModelResolverService.createModelReference(textModel.resource); + + let disposed = false; + Event.once(textModel.onWillDispose)(() => { + disposed = true; + }); + + ref1.dispose(); + await timeout(0); + assert.strictEqual(disposed, false); // not disposed because model still dirty + + const ref2 = await accessor.textModelResolverService.createModelReference(textModel.resource); + + textModel.revert(); + + await timeout(0); + assert.strictEqual(disposed, false); // not disposed because we got another ref meanwhile + + ref2.dispose(); + + await timeout(0); + assert.strictEqual(disposed, true); // now disposed because last ref got disposed + }); + + test('resolve untitled', async () => { + const service = accessor.untitledTextEditorService; + const untitledModel = service.create(); + const input = instantiationService.createInstance(UntitledTextEditorInput, untitledModel); + + await input.resolve(); + const ref = await accessor.textModelResolverService.createModelReference(input.resource); + const model = ref.object; + assert.strictEqual(untitledModel, model); + const editorModel = model.textEditorModel; + assert.ok(editorModel); + ref.dispose(); + input.dispose(); + model.dispose(); + }); + + test('even loading documents should be refcounted', async () => { + let resolveModel!: Function; + let waitForIt = new Promise(resolve => resolveModel = resolve); + + const disposable = accessor.textModelResolverService.registerTextModelContentProvider('test', { + provideTextContent: async (resource: URI): Promise => { + await waitForIt; + + let modelContent = 'Hello Test'; + let languageSelection = accessor.modeService.create('json'); + return accessor.modelService.createModel(modelContent, languageSelection, resource); + } + }); + + const uri = URI.from({ scheme: 'test', authority: null!, path: 'thePath' }); + + const modelRefPromise1 = accessor.textModelResolverService.createModelReference(uri); + const modelRefPromise2 = accessor.textModelResolverService.createModelReference(uri); + + resolveModel(); + + const modelRef1 = await modelRefPromise1; + const model1 = modelRef1.object; + const modelRef2 = await modelRefPromise2; + const model2 = modelRef2.object; + const textModel = model1.textEditorModel; + + assert.strictEqual(model1, model2, 'they are the same model'); + assert(!textModel.isDisposed(), 'the text model should not be disposed'); + + modelRef1.dispose(); + assert(!textModel.isDisposed(), 'the text model should still not be disposed'); + + let p1 = new Promise(resolve => textModel.onWillDispose(resolve)); + modelRef2.dispose(); + + await p1; + assert(textModel.isDisposed(), 'the text model should finally be disposed'); + + disposable.dispose(); + }); +}); diff --git a/src/vs/workbench/services/themes/browser/browserHostColorSchemeService.ts b/src/vs/workbench/services/themes/browser/browserHostColorSchemeService.ts index fa496f754b..b7dfa822c8 100644 --- a/src/vs/workbench/services/themes/browser/browserHostColorSchemeService.ts +++ b/src/vs/workbench/services/themes/browser/browserHostColorSchemeService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; +import * as dom from 'vs/base/browser/dom'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -25,10 +26,10 @@ export class BrowserHostColorSchemeService extends Disposable implements IHostCo private registerListeners(): void { - window.matchMedia('(prefers-color-scheme: dark)').addListener(() => { + dom.addMatchMediaChangeListener('(prefers-color-scheme: dark)', () => { this._onDidSchemeChangeEvent.fire(); }); - window.matchMedia('(forced-colors: active)').addListener(() => { + dom.addMatchMediaChangeListener('(forced-colors: active)', () => { this._onDidSchemeChangeEvent.fire(); }); } diff --git a/src/vs/workbench/services/themes/common/iconExtensionPoint.ts b/src/vs/workbench/services/themes/common/iconExtensionPoint.ts index c9017f4555..a52924da8e 100644 --- a/src/vs/workbench/services/themes/common/iconExtensionPoint.ts +++ b/src/vs/workbench/services/themes/common/iconExtensionPoint.ts @@ -29,7 +29,7 @@ interface IIconFontExtensionPoint { const iconRegistry: IIconRegistry = Registry.as(IconRegistryExtensions.IconContribution); const iconReferenceSchema = iconRegistry.getIconReferenceSchema(); -const iconIdPattern = `^${CSSIcon.iconNameSegment}-(${CSSIcon.iconNameSegment})+`; +const iconIdPattern = `^${CSSIcon.iconNameSegment}-(${CSSIcon.iconNameSegment})+$`; const iconConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'icons', @@ -38,6 +38,7 @@ const iconConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint { return this._nativeHostService.getWindowCount(); @@ -87,12 +92,28 @@ registerSingleton(ITimerService, TimerService); //#region cached data logic -export function didUseCachedData(): boolean { - // TODO@sandbox need a different way to figure out if cached data was used - if (process.sandboxed) { - return true; +const lastRunningCommitStorageKey = 'perf/lastRunningCommit'; +let _didUseCachedData: boolean | undefined = undefined; + +export function didUseCachedData(productService: IProductService, storageService: IStorageService, environmentService: INativeWorkbenchEnvironmentService): boolean { + // browser code loading: only a guess based on + // this being the first start with the commit + // or subsequent + if (isPreferringBrowserCodeLoad) { + if (typeof _didUseCachedData !== 'boolean') { + if (!environmentService.configuration.codeCachePath || !productService.commit) { + _didUseCachedData = false; // we only produce cached data whith commit and code cache path + } else if (storageService.get(lastRunningCommitStorageKey, StorageScope.GLOBAL) === productService.commit) { + _didUseCachedData = true; // subsequent start on same commit, assume cached data is there + } else { + storageService.store(lastRunningCommitStorageKey, productService.commit, StorageScope.GLOBAL, StorageTarget.MACHINE); + _didUseCachedData = false; // first time start on commit, assume cached data is not yet there + } + } + return _didUseCachedData; } - // We surely don't use cached data when we don't tell the loader to do so + // node.js code loading: We surely don't use cached data + // when we don't tell the loader to do so if (!Boolean((window).require.getConfig().nodeCachedData)) { return false; } diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorHandler.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorHandler.ts new file mode 100644 index 0000000000..d0e091bf2f --- /dev/null +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorHandler.ts @@ -0,0 +1,121 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Disposable } from 'vs/base/common/lifecycle'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { IEditorInputSerializer } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { isEqual, toLocalResource } from 'vs/base/common/resources'; +import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; + +interface ISerializedUntitledTextEditorInput { + resourceJSON: UriComponents; + modeId: string | undefined; + encoding: string | undefined; +} + +export class UntitledTextEditorInputSerializer implements IEditorInputSerializer { + + constructor( + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IPathService private readonly pathService: IPathService + ) { } + + canSerialize(editorInput: EditorInput): boolean { + return this.filesConfigurationService.isHotExitEnabled && !editorInput.isDisposed(); + } + + serialize(editorInput: EditorInput): string | undefined { + if (!this.filesConfigurationService.isHotExitEnabled || editorInput.isDisposed()) { + return undefined; + } + + const untitledTextEditorInput = editorInput as UntitledTextEditorInput; + + let resource = untitledTextEditorInput.resource; + if (untitledTextEditorInput.model.hasAssociatedFilePath) { + resource = toLocalResource(resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme); // untitled with associated file path use the local schema + } + + // Mode: only remember mode if it is either specific (not text) + // or if the mode was explicitly set by the user. We want to preserve + // this information across restarts and not set the mode unless + // this is the case. + let modeId: string | undefined; + const modeIdCandidate = untitledTextEditorInput.getMode(); + if (modeIdCandidate !== PLAINTEXT_MODE_ID) { + modeId = modeIdCandidate; + } else if (untitledTextEditorInput.model.hasModeSetExplicitly) { + modeId = modeIdCandidate; + } + + const serialized: ISerializedUntitledTextEditorInput = { + resourceJSON: resource.toJSON(), + modeId, + encoding: untitledTextEditorInput.getEncoding() + }; + + return JSON.stringify(serialized); + } + + deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): UntitledTextEditorInput { + return instantiationService.invokeFunction(accessor => { + const deserialized: ISerializedUntitledTextEditorInput = JSON.parse(serializedEditorInput); + const resource = URI.revive(deserialized.resourceJSON); + const mode = deserialized.modeId; + const encoding = deserialized.encoding; + + return accessor.get(IEditorService).createEditorInput({ resource, mode, encoding, forceUntitled: true }) as UntitledTextEditorInput; + }); + } +} + +export class UntitledTextEditorWorkingCopyEditorHandler extends Disposable implements IWorkbenchContribution { + + private static readonly UNTITLED_REGEX = /Untitled-\d+/; + + constructor( + @IWorkingCopyEditorService private readonly workingCopyEditorService: IWorkingCopyEditorService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IPathService private readonly pathService: IPathService, + @IEditorService private readonly editorService: IEditorService + ) { + super(); + + this.installHandler(); + } + + private installHandler(): void { + this._register(this.workingCopyEditorService.registerHandler({ + handles: workingCopy => workingCopy.resource.scheme === Schemas.untitled && workingCopy.typeId === NO_TYPE_ID, + isOpen: (workingCopy, editor) => editor instanceof UntitledTextEditorInput && isEqual(workingCopy.resource, editor.resource), + createEditor: workingCopy => { + let editorInputResource: URI; + + // This is a (weak) strategy to find out if the untitled input had + // an associated file path or not by just looking at the path. and + // if so, we must ensure to restore the local resource it had. + if (!UntitledTextEditorWorkingCopyEditorHandler.UNTITLED_REGEX.test(workingCopy.resource.path)) { + editorInputResource = toLocalResource(workingCopy.resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme); + } else { + editorInputResource = workingCopy.resource; + } + + return this.editorService.createEditorInput({ resource: editorInputResource, forceUntitled: true }); + } + })); + } +} diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorInput.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorInput.ts index 78d35d7633..0e4ddabe64 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorInput.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorInput.ts @@ -3,16 +3,17 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Verbosity } from 'vs/workbench/common/editor'; +import { GroupIdentifier, IUntitledTextResourceEditorInput, Verbosity } from 'vs/workbench/common/editor'; import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { EncodingMode, IEncodingSupport, IModeSupport, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ILabelService } from 'vs/platform/label/common/label'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IFileService } from 'vs/platform/files/common/files'; -import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; -import { isEqual } from 'vs/base/common/resources'; +import { isEqual, toLocalResource } from 'vs/base/common/resources'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { EditorOverride } from 'vs/platform/editor/common/editor'; /** * An editor input to be used for untitled text buffers. @@ -28,15 +29,15 @@ export class UntitledTextEditorInput extends AbstractTextResourceEditorInput imp private modelResolve: Promise | undefined = undefined; constructor( - public readonly model: IUntitledTextEditorModel, + readonly model: IUntitledTextEditorModel, @ITextFileService textFileService: ITextFileService, @ILabelService labelService: ILabelService, @IEditorService editorService: IEditorService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, @IFileService fileService: IFileService, - @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IPathService private readonly pathService: IPathService ) { - super(model.resource, undefined, editorService, editorGroupService, textFileService, labelService, fileService, filesConfigurationService); + super(model.resource, undefined, editorService, textFileService, labelService, fileService); this.registerModelListeners(model); } @@ -55,7 +56,7 @@ export class UntitledTextEditorInput extends AbstractTextResourceEditorInput imp return this.model.name; } - override getDescription(verbosity: Verbosity = Verbosity.MEDIUM): string | undefined { + override getDescription(verbosity = Verbosity.MEDIUM): string | undefined { // Without associated path: only use if name and description differ if (!this.model.hasAssociatedFilePath) { @@ -119,8 +120,22 @@ export class UntitledTextEditorInput extends AbstractTextResourceEditorInput imp return this.model; } + override asResourceEditorInput(group: GroupIdentifier): IUntitledTextResourceEditorInput { + return { + resource: this.model.hasAssociatedFilePath ? toLocalResource(this.model.resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme) : undefined, + forceUntitled: true, + encoding: this.getEncoding(), + mode: this.getMode(), + contents: this.model.isDirty() ? this.model.textEditorModel?.getValue() : undefined, + options: { + viewState: this.getViewStateFor(group), + override: EditorOverride.DISABLED + } + }; + } + override matches(otherInput: unknown): boolean { - if (otherInput === this) { + if (super.matches(otherInput)) { return true; } diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts index 47652d34f1..c5b39cf6aa 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts @@ -12,7 +12,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; import { ITextModel } from 'vs/editor/common/model'; -import { createTextBufferFactory, createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; +import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IWorkingCopy, WorkingCopyCapabilities, IWorkingCopyBackup, NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy'; @@ -63,12 +63,6 @@ export interface IUntitledTextEditorModel extends ITextEditorModel, IModeSupport * Resolves the untitled model. */ resolve(): Promise; - - /** - * Updates the value of the untitled model optionally allowing to ignore dirty. - * The model must be resolved for this method to work. - */ - setValue(value: string, ignoreDirty?: boolean): void; } export class UntitledTextEditorModel extends BaseTextEditorModel implements IUntitledTextEditorModel { @@ -99,6 +93,10 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt readonly capabilities = WorkingCopyCapabilities.Untitled; + //#region Name + + private configuredLabelFormat: 'content' | 'name' = 'content'; + private cachedModelFirstLineWords: string | undefined = undefined; get name(): string { // Take name from first line if present and only if @@ -112,17 +110,12 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt return this.labelService.getUriBasenameLabel(this.resource); } - private dirty = this.hasAssociatedFilePath || !!this.initialValue; - private ignoreDirtyOnModelContentChange = false; + //#endregion - private versionId = 0; - - private configuredEncoding: string | undefined; - private configuredLabelFormat: 'content' | 'name' = 'content'; constructor( - public readonly resource: URI, - public readonly hasAssociatedFilePath: boolean, + readonly resource: URI, + readonly hasAssociatedFilePath: boolean, private readonly initialValue: string | undefined, private preferredMode: string | undefined, private preferredEncoding: string | undefined, @@ -153,7 +146,7 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt private registerListeners(): void { // Config Changes - this._register(this.textResourceConfigurationService.onDidChangeConfiguration(e => this.onConfigurationChange(true))); + this._register(this.textResourceConfigurationService.onDidChangeConfiguration(() => this.onConfigurationChange(true))); } private onConfigurationChange(fromEvent: boolean): void { @@ -179,9 +172,8 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt } } - getVersionId(): number { - return this.versionId; - } + + //#region Mode private _hasModeSetExplicitly: boolean = false; get hasModeSetExplicitly(): boolean { return this._hasModeSetExplicitly; } @@ -216,6 +208,13 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt return this.preferredMode; } + //#endregion + + + //#region Encoding + + private configuredEncoding: string | undefined; + getEncoding(): string | undefined { return this.preferredEncoding || this.configuredEncoding; } @@ -230,25 +229,13 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt } } - setValue(value: string, ignoreDirty?: boolean): void { - if (ignoreDirty) { - this.ignoreDirtyOnModelContentChange = true; - } - - try { - this.updateTextEditorModel(createTextBufferFactory(value)); - } finally { - this.ignoreDirtyOnModelContentChange = false; - } - } - - override isReadonly(): boolean { - return false; - } + //#endregion //#region Dirty + private dirty = this.hasAssociatedFilePath || !!this.initialValue; + isDirty(): boolean { return this.dirty; } @@ -362,19 +349,16 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt } private onModelContentChanged(textEditorModel: ITextModel, e: IModelContentChangedEvent): void { - this.versionId++; - if (!this.ignoreDirtyOnModelContentChange) { - // mark the untitled text editor as non-dirty once its content becomes empty and we do - // not have an associated path set. we never want dirty indicator in that case. - if (!this.hasAssociatedFilePath && textEditorModel.getLineCount() === 1 && textEditorModel.getLineContent(1) === '') { - this.setDirty(false); - } + // mark the untitled text editor as non-dirty once its content becomes empty and we do + // not have an associated path set. we never want dirty indicator in that case. + if (!this.hasAssociatedFilePath && textEditorModel.getLineCount() === 1 && textEditorModel.getLineContent(1) === '') { + this.setDirty(false); + } - // turn dirty otherwise - else { - this.setDirty(true); - } + // turn dirty otherwise + else { + this.setDirty(true); } // Check for name change if first line changed in the range of 0-FIRST_LINE_NAME_CANDIDATE_MAX_LENGTH columns @@ -423,4 +407,9 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt } //#endregion + + + override isReadonly(): boolean { + return false; + } } diff --git a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts index f3cb2b3007..bd91c029b4 100644 --- a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts +++ b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts @@ -16,6 +16,7 @@ import { Range } from 'vs/editor/common/core/range'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { EditorInputCapabilities } from 'vs/workbench/common/editor'; suite('Untitled text editors', () => { @@ -42,9 +43,18 @@ suite('Untitled text editors', () => { assert.ok(service.get(input1.resource)); assert.ok(!service.get(URI.file('testing'))); + assert.ok(input1.hasCapability(EditorInputCapabilities.Untitled)); + assert.ok(!input1.hasCapability(EditorInputCapabilities.Readonly)); + assert.ok(!input1.hasCapability(EditorInputCapabilities.Singleton)); + assert.ok(!input1.hasCapability(EditorInputCapabilities.RequiresTrust)); + const input2 = instantiationService.createInstance(UntitledTextEditorInput, service.create()); assert.strictEqual(service.get(input2.resource), input2.model); + // asResourceEditorInput() + const untypedInput = input1.asResourceEditorInput(0); + assert.strictEqual(untypedInput.forceUntitled, true); + // get() assert.strictEqual(service.get(input1.resource), input1.model); assert.strictEqual(service.get(input2.resource), input2.model); @@ -71,6 +81,9 @@ suite('Untitled text editors', () => { assert.ok(input2.isDirty()); + const dirtyUntypedInput = input2.asResourceEditorInput(0); + assert.strictEqual(dirtyUntypedInput.contents, 'foo bar'); + assert.ok(workingCopyService.isDirty(input2.resource)); assert.strictEqual(workingCopyService.dirtyCount, 1); @@ -102,22 +115,6 @@ suite('Untitled text editors', () => { }); } - test('setValue()', async () => { - const service = accessor.untitledTextEditorService; - const untitled = instantiationService.createInstance(UntitledTextEditorInput, service.create()); - - const model = await untitled.resolve(); - - model.setValue('not dirty', true); - assert.ok(!model.isDirty()); - - model.setValue('dirty'); - assert.ok(model.isDirty()); - - untitled.dispose(); - model.dispose(); - }); - test('associated resource is dirty', async () => { const service = accessor.untitledTextEditorService; const file = URI.file(join('C:\\', '/foo/file.txt')); diff --git a/src/vs/workbench/services/url/browser/urlService.ts b/src/vs/workbench/services/url/browser/urlService.ts index 5505c6f73c..2661b410ef 100644 --- a/src/vs/workbench/services/url/browser/urlService.ts +++ b/src/vs/workbench/services/url/browser/urlService.ts @@ -9,6 +9,8 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { AbstractURLService } from 'vs/platform/url/common/urlService'; import { Event } from 'vs/base/common/event'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IOpenerService, IOpener, OpenExternalOptions, OpenInternalOptions, matchesScheme } from 'vs/platform/opener/common/opener'; +import { IProductService } from 'vs/platform/product/common/productService'; export interface IURLCallbackProvider { @@ -36,24 +38,44 @@ export interface IURLCallbackProvider { create(options?: Partial): URI; } +class BrowserURLOpener implements IOpener { + + constructor( + private urlService: IURLService, + private productService: IProductService + ) { } + + async open(resource: string | URI, options?: OpenInternalOptions | OpenExternalOptions): Promise { + if (!matchesScheme(resource, this.productService.urlProtocol)) { + return false; + } + + if (typeof resource === 'string') { + resource = URI.parse(resource); + } + + return this.urlService.open(resource, { trusted: true }); + } +} + export class BrowserURLService extends AbstractURLService { private provider: IURLCallbackProvider | undefined; constructor( - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IOpenerService openerService: IOpenerService, + @IProductService productService: IProductService ) { super(); this.provider = environmentService.options?.urlCallbackProvider; - this.registerListeners(); - } - - private registerListeners(): void { if (this.provider) { this._register(this.provider.onCallback(uri => this.open(uri, { trusted: true }))); } + + this._register(openerService.registerOpener(new BrowserURLOpener(this, productService))); } create(options?: Partial): URI { diff --git a/src/vs/workbench/services/userData/browser/userDataInit.ts b/src/vs/workbench/services/userData/browser/userDataInit.ts index 38446d9ea5..e9d2062ff4 100644 --- a/src/vs/workbench/services/userData/browser/userDataInit.ts +++ b/src/vs/workbench/services/userData/browser/userDataInit.ts @@ -109,7 +109,7 @@ export class UserDataInitializationService implements IUserDataInitializationSer const userDataSyncStore = this.userDataSyncStoreManagementService.userDataSyncStore; if (!userDataSyncStore) { this.logService.trace(`Skipping initializing user data as sync service is not provided`); - return undefined; + return undefined; // {{SQL CARBON EDIT}} strict-null-check } this.logService.info(`Using settings sync service ${userDataSyncStore.url.toString()} for initialization`); diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncResourceEnablementService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncResourceEnablementService.ts index 203ff7ebea..f199bebd8a 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncResourceEnablementService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncResourceEnablementService.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; diff --git a/src/vs/workbench/services/userDataSync/electron-sandbox/userDataSyncService.ts b/src/vs/workbench/services/userDataSync/electron-sandbox/userDataSyncService.ts index 59ca1f122e..2d88fc0f56 100644 --- a/src/vs/workbench/services/userDataSync/electron-sandbox/userDataSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-sandbox/userDataSyncService.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IUserDataSyncService } from 'vs/platform/userDataSync/common/userDataSync'; diff --git a/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts b/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts new file mode 100644 index 0000000000..19867e3787 --- /dev/null +++ b/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts @@ -0,0 +1,518 @@ +/*--------------------------------------------------------------------------------------------- + * 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 sinon from 'sinon'; +import { IViewsRegistry, IViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewContainerModel, IViewDescriptorService, ViewContainer } from 'vs/workbench/common/views'; +import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; +import { move } from 'vs/base/common/arrays'; +import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService'; +import { ViewDescriptorService } from 'vs/workbench/services/views/browser/viewDescriptorService'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; + +const ViewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); +const ViewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); + +class ViewDescriptorSequence { + + readonly elements: IViewDescriptor[]; + private disposables: IDisposable[] = []; + + constructor(model: IViewContainerModel) { + this.elements = [...model.visibleViewDescriptors]; + model.onDidAddVisibleViewDescriptors(added => added.forEach(({ viewDescriptor, index }) => this.elements.splice(index, 0, viewDescriptor)), null, this.disposables); + model.onDidRemoveVisibleViewDescriptors(removed => removed.sort((a, b) => b.index - a.index).forEach(({ index }) => this.elements.splice(index, 1)), null, this.disposables); + model.onDidMoveVisibleViewDescriptors(({ from, to }) => move(this.elements, from.index, to.index), null, this.disposables); + } + + dispose() { + this.disposables = dispose(this.disposables); + } +} + +suite('ViewContainerModel', () => { + + let container: ViewContainer; + let disposableStore: DisposableStore; + let contextKeyService: IContextKeyService; + let viewDescriptorService: IViewDescriptorService; + let storageService: IStorageService; + + setup(() => { + disposableStore = new DisposableStore(); + const instantiationService: TestInstantiationService = workbenchInstantiationService(); + contextKeyService = instantiationService.createInstance(ContextKeyService); + instantiationService.stub(IContextKeyService, contextKeyService); + storageService = instantiationService.get(IStorageService); + viewDescriptorService = instantiationService.createInstance(ViewDescriptorService); + }); + + teardown(() => { + disposableStore.dispose(); + ViewsRegistry.deregisterViews(ViewsRegistry.getViews(container), container); + ViewContainerRegistry.deregisterViewContainer(container); + }); + + test('empty model', function () { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', title: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + assert.strictEqual(testObject.visibleViewDescriptors.length, 0); + }); + + test('register/unregister', () => { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', title: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + + assert.strictEqual(testObject.visibleViewDescriptors.length, 0); + assert.strictEqual(target.elements.length, 0); + + const viewDescriptor: IViewDescriptor = { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1' + }; + + ViewsRegistry.registerViews([viewDescriptor], container); + + assert.strictEqual(testObject.visibleViewDescriptors.length, 1); + assert.strictEqual(target.elements.length, 1); + assert.deepStrictEqual(testObject.visibleViewDescriptors[0], viewDescriptor); + assert.deepStrictEqual(target.elements[0], viewDescriptor); + + ViewsRegistry.deregisterViews([viewDescriptor], container); + + assert.strictEqual(testObject.visibleViewDescriptors.length, 0); + assert.strictEqual(target.elements.length, 0); + }); + + test('when contexts', async function () { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', title: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + assert.strictEqual(testObject.visibleViewDescriptors.length, 0); + assert.strictEqual(target.elements.length, 0); + + const viewDescriptor: IViewDescriptor = { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + when: ContextKeyExpr.equals('showview1', true) + }; + + ViewsRegistry.registerViews([viewDescriptor], container); + assert.strictEqual(testObject.visibleViewDescriptors.length, 0, 'view should not appear since context isnt in'); + assert.strictEqual(target.elements.length, 0); + + const key = contextKeyService.createKey('showview1', false); + assert.strictEqual(testObject.visibleViewDescriptors.length, 0, 'view should still not appear since showview1 isnt true'); + assert.strictEqual(target.elements.length, 0); + + key.set(true); + await new Promise(c => setTimeout(c, 30)); + assert.strictEqual(testObject.visibleViewDescriptors.length, 1, 'view should appear'); + assert.strictEqual(target.elements.length, 1); + assert.deepStrictEqual(testObject.visibleViewDescriptors[0], viewDescriptor); + assert.strictEqual(target.elements[0], viewDescriptor); + + key.set(false); + await new Promise(c => setTimeout(c, 30)); + assert.strictEqual(testObject.visibleViewDescriptors.length, 0, 'view should disappear'); + assert.strictEqual(target.elements.length, 0); + + ViewsRegistry.deregisterViews([viewDescriptor], container); + assert.strictEqual(testObject.visibleViewDescriptors.length, 0, 'view should not be there anymore'); + assert.strictEqual(target.elements.length, 0); + + key.set(true); + await new Promise(c => setTimeout(c, 30)); + assert.strictEqual(testObject.visibleViewDescriptors.length, 0, 'view should not be there anymore'); + assert.strictEqual(target.elements.length, 0); + }); + + test('when contexts - multiple', async function () { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', title: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + const view1: IViewDescriptor = { id: 'view1', ctorDescriptor: null!, name: 'Test View 1' }; + const view2: IViewDescriptor = { id: 'view2', ctorDescriptor: null!, name: 'Test View 2', when: ContextKeyExpr.equals('showview2', true) }; + + ViewsRegistry.registerViews([view1, view2], container); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [view1], 'only view1 should be visible'); + assert.deepStrictEqual(target.elements, [view1], 'only view1 should be visible'); + + const key = contextKeyService.createKey('showview2', false); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [view1], 'still only view1 should be visible'); + assert.deepStrictEqual(target.elements, [view1], 'still only view1 should be visible'); + + key.set(true); + await new Promise(c => setTimeout(c, 30)); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [view1, view2], 'both views should be visible'); + assert.deepStrictEqual(target.elements, [view1, view2], 'both views should be visible'); + + ViewsRegistry.deregisterViews([view1, view2], container); + }); + + test('when contexts - multiple 2', async function () { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', title: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + const view1: IViewDescriptor = { id: 'view1', ctorDescriptor: null!, name: 'Test View 1', when: ContextKeyExpr.equals('showview1', true) }; + const view2: IViewDescriptor = { id: 'view2', ctorDescriptor: null!, name: 'Test View 2' }; + + ViewsRegistry.registerViews([view1, view2], container); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [view2], 'only view2 should be visible'); + assert.deepStrictEqual(target.elements, [view2], 'only view2 should be visible'); + + const key = contextKeyService.createKey('showview1', false); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [view2], 'still only view2 should be visible'); + assert.deepStrictEqual(target.elements, [view2], 'still only view2 should be visible'); + + key.set(true); + await new Promise(c => setTimeout(c, 30)); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [view1, view2], 'both views should be visible'); + assert.deepStrictEqual(target.elements, [view1, view2], 'both views should be visible'); + + ViewsRegistry.deregisterViews([view1, view2], container); + }); + + test('setVisible', () => { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', title: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + const view1: IViewDescriptor = { id: 'view1', ctorDescriptor: null!, name: 'Test View 1', canToggleVisibility: true }; + const view2: IViewDescriptor = { id: 'view2', ctorDescriptor: null!, name: 'Test View 2', canToggleVisibility: true }; + const view3: IViewDescriptor = { id: 'view3', ctorDescriptor: null!, name: 'Test View 3', canToggleVisibility: true }; + + ViewsRegistry.registerViews([view1, view2, view3], container); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [view1, view2, view3]); + assert.deepStrictEqual(target.elements, [view1, view2, view3]); + + testObject.setVisible('view2', true); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [view1, view2, view3], 'nothing should happen'); + assert.deepStrictEqual(target.elements, [view1, view2, view3]); + + testObject.setVisible('view2', false); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [view1, view3], 'view2 should hide'); + assert.deepStrictEqual(target.elements, [view1, view3]); + + testObject.setVisible('view1', false); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [view3], 'view1 should hide'); + assert.deepStrictEqual(target.elements, [view3]); + + testObject.setVisible('view3', false); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [], 'view3 shoud hide'); + assert.deepStrictEqual(target.elements, []); + + testObject.setVisible('view1', true); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [view1], 'view1 should show'); + assert.deepStrictEqual(target.elements, [view1]); + + testObject.setVisible('view3', true); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [view1, view3], 'view3 should show'); + assert.deepStrictEqual(target.elements, [view1, view3]); + + testObject.setVisible('view2', true); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [view1, view2, view3], 'view2 should show'); + assert.deepStrictEqual(target.elements, [view1, view2, view3]); + + ViewsRegistry.deregisterViews([view1, view2, view3], container); + assert.deepStrictEqual(testObject.visibleViewDescriptors, []); + assert.deepStrictEqual(target.elements, []); + }); + + test('move', () => { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', title: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + const view1: IViewDescriptor = { id: 'view1', ctorDescriptor: null!, name: 'Test View 1' }; + const view2: IViewDescriptor = { id: 'view2', ctorDescriptor: null!, name: 'Test View 2' }; + const view3: IViewDescriptor = { id: 'view3', ctorDescriptor: null!, name: 'Test View 3' }; + + ViewsRegistry.registerViews([view1, view2, view3], container); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [view1, view2, view3], 'model views should be OK'); + assert.deepStrictEqual(target.elements, [view1, view2, view3], 'sql views should be OK'); + + testObject.move('view3', 'view1'); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [view3, view1, view2], 'view3 should go to the front'); + assert.deepStrictEqual(target.elements, [view3, view1, view2]); + + testObject.move('view1', 'view2'); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [view3, view2, view1], 'view1 should go to the end'); + assert.deepStrictEqual(target.elements, [view3, view2, view1]); + + testObject.move('view1', 'view3'); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [view1, view3, view2], 'view1 should go to the front'); + assert.deepStrictEqual(target.elements, [view1, view3, view2]); + + testObject.move('view2', 'view3'); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [view1, view2, view3], 'view2 should go to the middle'); + assert.deepStrictEqual(target.elements, [view1, view2, view3]); + }); + + test('view states', async function () { + storageService.store(`${container.id}.state.hidden`, JSON.stringify([{ id: 'view1', isHidden: true }]), StorageScope.GLOBAL, StorageTarget.MACHINE); + container = ViewContainerRegistry.registerViewContainer({ id: 'test', title: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + + assert.strictEqual(testObject.visibleViewDescriptors.length, 0); + assert.strictEqual(target.elements.length, 0); + + const viewDescriptor: IViewDescriptor = { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1' + }; + + ViewsRegistry.registerViews([viewDescriptor], container); + assert.strictEqual(testObject.visibleViewDescriptors.length, 0, 'view should not appear since it was set not visible in view state'); + assert.strictEqual(target.elements.length, 0); + }); + + test('view states and when contexts', async function () { + storageService.store(`${container.id}.state.hidden`, JSON.stringify([{ id: 'view1', isHidden: true }]), StorageScope.GLOBAL, StorageTarget.MACHINE); + container = ViewContainerRegistry.registerViewContainer({ id: 'test', title: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + + assert.strictEqual(testObject.visibleViewDescriptors.length, 0); + assert.strictEqual(target.elements.length, 0); + + const viewDescriptor: IViewDescriptor = { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + when: ContextKeyExpr.equals('showview1', true) + }; + + ViewsRegistry.registerViews([viewDescriptor], container); + assert.strictEqual(testObject.visibleViewDescriptors.length, 0, 'view should not appear since context isnt in'); + assert.strictEqual(target.elements.length, 0); + + const key = contextKeyService.createKey('showview1', false); + assert.strictEqual(testObject.visibleViewDescriptors.length, 0, 'view should still not appear since showview1 isnt true'); + assert.strictEqual(target.elements.length, 0); + + key.set(true); + await new Promise(c => setTimeout(c, 30)); + assert.strictEqual(testObject.visibleViewDescriptors.length, 0, 'view should still not appear since it was set not visible in view state'); + assert.strictEqual(target.elements.length, 0); + }); + + test('view states and when contexts multiple views', async function () { + storageService.store(`${container.id}.state.hidden`, JSON.stringify([{ id: 'view1', isHidden: true }]), StorageScope.GLOBAL, StorageTarget.MACHINE); + container = ViewContainerRegistry.registerViewContainer({ id: 'test', title: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + + assert.strictEqual(testObject.visibleViewDescriptors.length, 0); + assert.strictEqual(target.elements.length, 0); + + const view1: IViewDescriptor = { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + when: ContextKeyExpr.equals('showview', true) + }; + const view2: IViewDescriptor = { + id: 'view2', + ctorDescriptor: null!, + name: 'Test View 2', + }; + const view3: IViewDescriptor = { + id: 'view3', + ctorDescriptor: null!, + name: 'Test View 3', + when: ContextKeyExpr.equals('showview', true) + }; + + ViewsRegistry.registerViews([view1, view2, view3], container); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [view2], 'Only view2 should be visible'); + assert.deepStrictEqual(target.elements, [view2]); + + const key = contextKeyService.createKey('showview', false); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [view2], 'Only view2 should be visible'); + assert.deepStrictEqual(target.elements, [view2]); + + key.set(true); + await new Promise(c => setTimeout(c, 30)); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [view2, view3], 'view3 should be visible'); + assert.deepStrictEqual(target.elements, [view2, view3]); + + key.set(false); + await new Promise(c => setTimeout(c, 30)); + assert.deepStrictEqual(testObject.visibleViewDescriptors, [view2], 'Only view2 should be visible'); + assert.deepStrictEqual(target.elements, [view2]); + }); + + test('remove event is not triggered if view was hidden and removed', async function () { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', title: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + const viewDescriptor: IViewDescriptor = { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + when: ContextKeyExpr.equals('showview1', true), + canToggleVisibility: true + }; + + ViewsRegistry.registerViews([viewDescriptor], container); + + const key = contextKeyService.createKey('showview1', true); + await new Promise(c => setTimeout(c, 30)); + assert.strictEqual(testObject.visibleViewDescriptors.length, 1, 'view should appear after context is set'); + assert.strictEqual(target.elements.length, 1); + + testObject.setVisible('view1', false); + assert.strictEqual(testObject.visibleViewDescriptors.length, 0, 'view should disappear after setting visibility to false'); + assert.strictEqual(target.elements.length, 0); + + const targetEvent = sinon.spy(testObject.onDidRemoveVisibleViewDescriptors); + key.set(false); + await new Promise(c => setTimeout(c, 30)); + assert.ok(!targetEvent.called, 'remove event should not be called since it is already hidden'); + }); + + test('add event is not triggered if view was set visible (when visible) and not active', async function () { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', title: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + const viewDescriptor: IViewDescriptor = { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + when: ContextKeyExpr.equals('showview1', true), + canToggleVisibility: true + }; + + const key = contextKeyService.createKey('showview1', true); + key.set(false); + ViewsRegistry.registerViews([viewDescriptor], container); + + assert.strictEqual(testObject.visibleViewDescriptors.length, 0); + assert.strictEqual(target.elements.length, 0); + + const targetEvent = sinon.spy(testObject.onDidAddVisibleViewDescriptors); + testObject.setVisible('view1', true); + assert.ok(!targetEvent.called, 'add event should not be called since it is already visible'); + assert.strictEqual(testObject.visibleViewDescriptors.length, 0); + assert.strictEqual(target.elements.length, 0); + }); + + test('remove event is not triggered if view was hidden and not active', async function () { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', title: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + const viewDescriptor: IViewDescriptor = { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + when: ContextKeyExpr.equals('showview1', true), + canToggleVisibility: true + }; + + const key = contextKeyService.createKey('showview1', true); + key.set(false); + ViewsRegistry.registerViews([viewDescriptor], container); + + assert.strictEqual(testObject.visibleViewDescriptors.length, 0); + assert.strictEqual(target.elements.length, 0); + + const targetEvent = sinon.spy(testObject.onDidAddVisibleViewDescriptors); + testObject.setVisible('view1', false); + assert.ok(!targetEvent.called, 'add event should not be called since it is disabled'); + assert.strictEqual(testObject.visibleViewDescriptors.length, 0); + assert.strictEqual(target.elements.length, 0); + }); + + test('add event is not triggered if view was set visible (when not visible) and not active', async function () { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', title: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + const viewDescriptor: IViewDescriptor = { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + when: ContextKeyExpr.equals('showview1', true), + canToggleVisibility: true + }; + + const key = contextKeyService.createKey('showview1', true); + key.set(false); + ViewsRegistry.registerViews([viewDescriptor], container); + + assert.strictEqual(testObject.visibleViewDescriptors.length, 0); + assert.strictEqual(target.elements.length, 0); + + testObject.setVisible('view1', false); + assert.strictEqual(testObject.visibleViewDescriptors.length, 0); + assert.strictEqual(target.elements.length, 0); + + const targetEvent = sinon.spy(testObject.onDidAddVisibleViewDescriptors); + testObject.setVisible('view1', true); + assert.ok(!targetEvent.called, 'add event should not be called since it is disabled'); + assert.strictEqual(testObject.visibleViewDescriptors.length, 0); + assert.strictEqual(target.elements.length, 0); + }); + + test('added view descriptors are in ascending order in the event', async function () { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', title: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + + ViewsRegistry.registerViews([{ + id: 'view5', + ctorDescriptor: null!, + name: 'Test View 5', + canToggleVisibility: true, + order: 5 + }, { + id: 'view2', + ctorDescriptor: null!, + name: 'Test View 2', + canToggleVisibility: true, + order: 2 + }], container); + + assert.strictEqual(target.elements.length, 2); + assert.strictEqual(target.elements[0].id, 'view2'); + assert.strictEqual(target.elements[1].id, 'view5'); + + ViewsRegistry.registerViews([{ + id: 'view4', + ctorDescriptor: null!, + name: 'Test View 4', + canToggleVisibility: true, + order: 4 + }, { + id: 'view3', + ctorDescriptor: null!, + name: 'Test View 3', + canToggleVisibility: true, + order: 3 + }, { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + canToggleVisibility: true, + order: 1 + }], container); + + assert.strictEqual(target.elements.length, 5); + assert.strictEqual(target.elements[0].id, 'view1'); + assert.strictEqual(target.elements[1].id, 'view2'); + assert.strictEqual(target.elements[2].id, 'view3'); + assert.strictEqual(target.elements[3].id, 'view4'); + assert.strictEqual(target.elements[4].id, 'view5'); + }); + +}); diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts new file mode 100644 index 0000000000..ecfdf223a5 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts @@ -0,0 +1,169 @@ +/*--------------------------------------------------------------------------------------------- + * 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, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; +import { Promises } from 'vs/base/common/async'; +import { IFileService } from 'vs/platform/files/common/files'; +import { URI } from 'vs/base/common/uri'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { IFileWorkingCopy, IFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; + +export interface IBaseFileWorkingCopyManager> extends IDisposable { + + /** + * An event for when a file working copy was created. + */ + readonly onDidCreate: Event; + + /** + * Access to all known file working copies within the manager. + */ + readonly workingCopies: readonly W[]; + + /** + * Returns the file working copy for the provided resource + * or `undefined` if none. + */ + get(resource: URI): W | undefined; + + /** + * Disposes all working copies of the manager and disposes the manager. This + * method is different from `dispose` in that it will unregister any working + * copy from the `IWorkingCopyService`. Since this impact things like backups, + * the method is `async` because it needs to trigger `save` for any dirty + * working copy to preserve the data. + * + * Callers should make sure to e.g. close any editors associated with the + * working copy. + */ + destroy(): Promise; +} + +export abstract class BaseFileWorkingCopyManager> extends Disposable implements IBaseFileWorkingCopyManager { + + private readonly _onDidCreate = this._register(new Emitter()); + readonly onDidCreate = this._onDidCreate.event; + + private readonly mapResourceToWorkingCopy = new ResourceMap(); + private readonly mapResourceToDisposeListener = new ResourceMap(); + + constructor( + @IFileService protected readonly fileService: IFileService, + @ILogService protected readonly logService: ILogService, + @IWorkingCopyBackupService protected readonly workingCopyBackupService: IWorkingCopyBackupService + ) { + super(); + } + + protected has(resource: URI): boolean { + return this.mapResourceToWorkingCopy.has(resource); + } + + protected add(resource: URI, workingCopy: W): void { + const knownWorkingCopy = this.get(resource); + if (knownWorkingCopy === workingCopy) { + return; // already cached + } + + // Add to our working copy map + this.mapResourceToWorkingCopy.set(resource, workingCopy); + + // Update our dipsose listener to remove it on dispose + this.mapResourceToDisposeListener.get(resource)?.dispose(); + this.mapResourceToDisposeListener.set(resource, workingCopy.onWillDispose(() => this.remove(resource))); + + // Signal creation event + this._onDidCreate.fire(workingCopy); + } + + protected remove(resource: URI): void { + + // Dispose any existing listener + const disposeListener = this.mapResourceToDisposeListener.get(resource); + if (disposeListener) { + dispose(disposeListener); + this.mapResourceToDisposeListener.delete(resource); + } + + // Remove from our working copy map + this.mapResourceToWorkingCopy.delete(resource); + } + + //#region Get / Get all + + get workingCopies(): W[] { + return [...this.mapResourceToWorkingCopy.values()]; + } + + get(resource: URI): W | undefined { + return this.mapResourceToWorkingCopy.get(resource); + } + + //#endregion + + //#region Lifecycle + + override dispose(): void { + super.dispose(); + + // Clear working copy caches + // + // Note: we are not explicitly disposing the working copies + // known to the manager because this can have unwanted side + // effects such as backups getting discarded once the working + // copy unregisters. We have an explicit `destroy` + // for that purpose (https://github.com/microsoft/vscode/pull/123555) + // + this.mapResourceToWorkingCopy.clear(); + + // Dispose the dispose listeners + dispose(this.mapResourceToDisposeListener.values()); + this.mapResourceToDisposeListener.clear(); + } + + async destroy(): Promise { + + // Make sure all dirty working copies are saved to disk + try { + await Promises.settled(this.workingCopies.map(async workingCopy => { + if (workingCopy.isDirty()) { + await this.saveWithFallback(workingCopy); + } + })); + } catch (error) { + this.logService.error(error); + } + + // Dispose all working copies + dispose(this.mapResourceToWorkingCopy.values()); + + // Finally dispose manager + this.dispose(); + } + + private async saveWithFallback(workingCopy: W): Promise { + + // First try regular save + let saveFailed = false; + try { + await workingCopy.save(); + } catch (error) { + saveFailed = true; + } + + // Then fallback to backup if that exists + if (saveFailed || workingCopy.isDirty()) { + const backup = await this.workingCopyBackupService.resolve(workingCopy); + if (backup) { + await this.fileService.writeFile(workingCopy.resource, backup.value, { unlock: true }); + } + } + } + + //#endregion +} diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts index 0c2b644b3d..8085efaa03 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts @@ -1,95 +1,55 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; -import { URI } from 'vs/base/common/uri'; -import { Event, Emitter } from 'vs/base/common/event'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { ETAG_DISABLED, FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult, FileSystemProviderCapabilities, IFileService, IFileStatWithMetadata, IFileStreamContent, IWriteFileOptions } from 'vs/platform/files/common/files'; -import { ISaveOptions, IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; -import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { IWorkingCopy, IWorkingCopyBackup, IWorkingCopyBackupMeta, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; -import { raceCancellation, TaskSequentializer, timeout } from 'vs/base/common/async'; -import { ILogService } from 'vs/platform/log/common/log'; -import { assertIsDefined } from 'vs/base/common/types'; -import { ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { Event } from 'vs/base/common/event'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { VSBufferReadableStream } from 'vs/base/common/buffer'; -import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; -import { IWorkingCopyBackupService, IResolvedWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { hash } from 'vs/base/common/hash'; -import { toErrorMessage } from 'vs/base/common/errorMessage'; -import { IAction, toAction } from 'vs/base/common/actions'; -import { isWindows } from 'vs/base/common/platform'; -import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IElevatedFileService } from 'vs/workbench/services/files/common/elevatedFileService'; +import { URI } from 'vs/base/common/uri'; +import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy'; -export interface IFileWorkingCopyModelFactory { +export interface IFileWorkingCopyModelFactory { /** - * Asks the file working copy delegate to create a model from the given - * content under the provided resource. The content may originate from - * different sources depending on context: - * - from a backup if that exists - * - from the underlying file resource - * - passed in from the caller + * Create a model for the untitled or stored working copy + * from the given content under the provided resource. * * @param resource the `URI` of the model * @param contents the content of the model to create it * @param token support for cancellation */ - createModel(resource: URI, contents: VSBufferReadableStream, token: CancellationToken): Promise; + createModel(resource: URI, contents: VSBufferReadableStream, token: CancellationToken): Promise; } /** - * The underlying model of a file working copy provides some - * methods for the file working copy to function. The model is - * typically only available after the working copy has been - * resolved via it's `resolve()` method. + * A generic file working copy model to be reused by untitled + * and stored file working copies. */ export interface IFileWorkingCopyModel extends IDisposable { /** - * This event signals ANY changes to the contents of the file - * working copy model, for example: + * This event signals ANY changes to the contents, for example: * - through the user typing into the editor * - from API usage (e.g. bulk edits) * - when `IFileWorkingCopyModel#update` is invoked with contents * that are different from the current contents * - * The file working copy will listen to these changes and mark + * The file working copy will listen to these changes and may mark * the working copy as dirty whenever this event fires. * * Note: ONLY report changes to the model but not the underlying * file. The file working copy is tracking changes to the file * automatically. */ - readonly onDidChangeContent: Event; + readonly onDidChangeContent: Event; /** * An event emitted right before disposing the model. */ readonly onWillDispose: Event; - /** - * A version ID of the model. If a `onDidChangeContent` is fired - * from the model and the last known saved `versionId` matches - * with the `model.versionId`, the file working copy will discard - * any dirty state. - * - * A use case is the following: - * - a file working copy gets edited and thus dirty - * - the user triggers undo to revert the changes - * - at this point the `versionId` should match the one we had saved - * - * This requires the model to be aware of undo/redo operations. - */ - readonly versionId: unknown; - /** * Snapshots the model's current content for writing. This must include * any changes that were made to the model that are in memory. @@ -111,64 +71,15 @@ export interface IFileWorkingCopyModel extends IDisposable { * @param token support for cancellation */ update(contents: VSBufferReadableStream, token: CancellationToken): Promise; - - /** - * Close the current undo-redo element. This offers a way - * to create an undo/redo stop point. - * - * This method may for example be called right before the - * save is triggered so that the user can always undo back - * to the state before saving. - */ - pushStackElement(): void; } -export interface IFileWorkingCopyModelContentChangedEvent { +export interface IFileWorkingCopy extends IWorkingCopy, IDisposable { /** - * Flag that indicates that this event was generated while undoing. - */ - readonly isUndoing: boolean; - - /** - * Flag that indicates that this event was generated while redoing. - */ - readonly isRedoing: boolean; -} - -/** - * A file based `IWorkingCopy` is backed by a `URI` from a - * known file system provider. Given this assumption, a lot - * of functionality can be built on top, such as saving in - * a secure way to prevent data loss. - */ -export interface IFileWorkingCopy extends IWorkingCopy, Disposable { - - /** - * An event for when a file working copy was resolved. - */ - readonly onDidResolve: Event; - - /** - * An event for when a file working copy was saved successfully. - */ - readonly onDidSave: Event; - - /** - * An event indicating that a file working copy save operation failed. - */ - readonly onDidSaveError: Event; - - /** - * An event for when the file working copy was reverted. + * An event for when the file working copy has been reverted. */ readonly onDidRevert: Event; - /** - * An event for when the orphaned state of the file working copy changes. - */ - readonly onDidChangeOrphaned: Event; - /** * An event for when the file working copy has been disposed. */ @@ -179,1172 +90,24 @@ export interface IFileWorkingCopy extends IWork * based working copy. As long as the file working copy * has not been resolved, the model is `undefined`. */ - readonly model: T | undefined; + readonly model: M | undefined; /** - * Resolves a file working copy. + * Resolves the file working copy and thus makes the `model` + * available. */ - resolve(options?: IFileWorkingCopyResolveOptions): Promise; - - /** - * Explicitly sets the working copy to be dirty. - */ - markDirty(): void; - - /** - * Whether the file working copy is in the provided `state` - * or not. - * - * @param state the `FileWorkingCopyState` to check on. - */ - hasState(state: FileWorkingCopyState): boolean; - - /** - * Allows to join a state change away from the provided `state`. - * - * @param state currently only `FileWorkingCopyState.PENDING_SAVE` - * can be awaited on to resolve. - */ - joinState(state: FileWorkingCopyState.PENDING_SAVE): Promise; + resolve(): Promise; /** * Whether we have a resolved model or not. */ - isResolved(): this is IResolvedFileWorkingCopy; - - /** - * Whether the file working copy has been disposed or not. - */ - isDisposed(): boolean; + isResolved(): this is IResolvedFileWorkingCopy; } -export interface IResolvedFileWorkingCopy extends IFileWorkingCopy { +export interface IResolvedFileWorkingCopy extends IFileWorkingCopy { /** - * A resolved file working copy has a resolved model `T`. + * A resolved file working copy has a resolved model. */ - readonly model: T; -} - -/** - * States the file working copy can be in. - */ -export const enum FileWorkingCopyState { - - /** - * A file working copy is saved. - */ - SAVED, - - /** - * A file working copy is dirty. - */ - DIRTY, - - /** - * A file working copy is currently being saved but - * this operation has not completed yet. - */ - PENDING_SAVE, - - /** - * A file working copy is in conflict mode when changes - * cannot be saved because the underlying file has changed. - * File working copies in conflict mode are always dirty. - */ - CONFLICT, - - /** - * A file working copy is in orphan state when the underlying - * file has been deleted. - */ - ORPHAN, - - /** - * Any error that happens during a save that is not causing - * the `FileWorkingCopyState.CONFLICT` state. - * File working copies in error mode are always dirty. - */ - ERROR -} - -export interface IFileWorkingCopySaveOptions extends ISaveOptions { - - /** - * Save the file working copy with an attempt to unlock it. - */ - writeUnlock?: boolean; - - /** - * Save the file working copy with elevated privileges. - * - * Note: This may not be supported in all environments. - */ - writeElevated?: boolean; - - /** - * Allows to write to a file working copy even if it has been - * modified on disk. This should only be triggered from an - * explicit user action. - */ - ignoreModifiedSince?: boolean; - - /** - * If set, will bubble up the file working copy save error to - * the caller instead of handling it. - */ - ignoreErrorHandler?: boolean; -} - -export interface IFileWorkingCopyResolveOptions { - - /** - * The contents to use for the file working copy if known. If not - * provided, the contents will be retrieved from the underlying - * resource or backup if present. - * - * If contents are provided, the file working copy will be marked - * as dirty right from the beginning. - */ - contents?: VSBufferReadableStream; - - /** - * Go to disk bypassing any cache of the file working copy if any. - */ - forceReadFromFile?: boolean; -} - -/** - * Metadata associated with a file working copy backup. - */ -interface IFileWorkingCopyBackupMetaData extends IWorkingCopyBackupMeta { - mtime: number; - ctime: number; - size: number; - etag: string; - orphaned: boolean; -} - -export class FileWorkingCopy extends Disposable implements IFileWorkingCopy { - - readonly capabilities: WorkingCopyCapabilities = WorkingCopyCapabilities.None; - - private _model: T | undefined = undefined; - get model(): T | undefined { return this._model; } - - //#region events - - private readonly _onDidChangeContent = this._register(new Emitter()); - readonly onDidChangeContent = this._onDidChangeContent.event; - - private readonly _onDidResolve = this._register(new Emitter()); - readonly onDidResolve = this._onDidResolve.event; - - private readonly _onDidChangeDirty = this._register(new Emitter()); - readonly onDidChangeDirty = this._onDidChangeDirty.event; - - private readonly _onDidSaveError = this._register(new Emitter()); - readonly onDidSaveError = this._onDidSaveError.event; - - private readonly _onDidSave = this._register(new Emitter()); - readonly onDidSave = this._onDidSave.event; - - private readonly _onDidRevert = this._register(new Emitter()); - readonly onDidRevert = this._onDidRevert.event; - - private readonly _onDidChangeOrphaned = this._register(new Emitter()); - readonly onDidChangeOrphaned = this._onDidChangeOrphaned.event; - - private readonly _onWillDispose = this._register(new Emitter()); - readonly onWillDispose = this._onWillDispose.event; - - //#endregion - - constructor( - readonly typeId: string, - readonly resource: URI, - readonly name: string, - private readonly modelFactory: IFileWorkingCopyModelFactory, - @IFileService private readonly fileService: IFileService, - @ILogService private readonly logService: ILogService, - @ITextFileService private readonly textFileService: ITextFileService, - @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, - @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, - @IWorkingCopyService workingCopyService: IWorkingCopyService, - @INotificationService private readonly notificationService: INotificationService, - @IWorkingCopyEditorService private readonly workingCopyEditorService: IWorkingCopyEditorService, - @IEditorService private readonly editorService: IEditorService, - @IElevatedFileService private readonly elevatedFileService: IElevatedFileService - ) { - super(); - - if (!fileService.canHandleResource(this.resource)) { - throw new Error(`The file working copy resource ${this.resource.toString(true)} does not have an associated file system provider.`); - } - - // Make known to working copy service - this._register(workingCopyService.registerWorkingCopy(this)); - - this.registerListeners(); - } - - //#region Orphaned Tracking - - private inOrphanMode = false; - - private registerListeners(): void { - this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e))); - } - - private async onDidFilesChange(e: FileChangesEvent): Promise { - let fileEventImpactsUs = false; - let newInOrphanModeGuess: boolean | undefined; - - // If we are currently orphaned, we check if the file was added back - if (this.inOrphanMode) { - const fileWorkingCopyResourceAdded = e.contains(this.resource, FileChangeType.ADDED); - if (fileWorkingCopyResourceAdded) { - newInOrphanModeGuess = false; - fileEventImpactsUs = true; - } - } - - // Otherwise we check if the file was deleted - else { - const fileWorkingCopyResourceDeleted = e.contains(this.resource, FileChangeType.DELETED); - if (fileWorkingCopyResourceDeleted) { - newInOrphanModeGuess = true; - fileEventImpactsUs = true; - } - } - - if (fileEventImpactsUs && this.inOrphanMode !== newInOrphanModeGuess) { - let newInOrphanModeValidated: boolean = false; - if (newInOrphanModeGuess) { - - // We have received reports of users seeing delete events even though the file still - // exists (network shares issue: https://github.com/microsoft/vscode/issues/13665). - // Since we do not want to mark the working copy as orphaned, we have to check if the - // file is really gone and not just a faulty file event. - await timeout(100); - - if (this.isDisposed()) { - newInOrphanModeValidated = true; - } else { - const exists = await this.fileService.exists(this.resource); - newInOrphanModeValidated = !exists; - } - } - - if (this.inOrphanMode !== newInOrphanModeValidated && !this.isDisposed()) { - this.setOrphaned(newInOrphanModeValidated); - } - } - } - - private setOrphaned(orphaned: boolean): void { - if (this.inOrphanMode !== orphaned) { - this.inOrphanMode = orphaned; - - this._onDidChangeOrphaned.fire(); - } - } - - //#endregion - - //#region Dirty - - private dirty = false; - private savedVersionId: unknown; - - isDirty(): this is IResolvedFileWorkingCopy { - return this.dirty; - } - - markDirty(): void { - this.setDirty(true); - } - - private setDirty(dirty: boolean): void { - if (!this.isResolved()) { - return; // only resolved working copies can be marked dirty - } - - // Track dirty state and version id - const wasDirty = this.dirty; - this.doSetDirty(dirty); - - // Emit as Event if dirty changed - if (dirty !== wasDirty) { - this._onDidChangeDirty.fire(); - } - } - - private doSetDirty(dirty: boolean): () => void { - const wasDirty = this.dirty; - const wasInConflictMode = this.inConflictMode; - const wasInErrorMode = this.inErrorMode; - const oldSavedVersionId = this.savedVersionId; - - if (!dirty) { - this.dirty = false; - this.inConflictMode = false; - this.inErrorMode = false; - - // we remember the models alternate version id to remember when the version - // of the model matches with the saved version on disk. we need to keep this - // in order to find out if the model changed back to a saved version (e.g. - // when undoing long enough to reach to a version that is saved and then to - // clear the dirty flag) - if (this.isResolved()) { - this.savedVersionId = this.model.versionId; - } - } else { - this.dirty = true; - } - - // Return function to revert this call - return () => { - this.dirty = wasDirty; - this.inConflictMode = wasInConflictMode; - this.inErrorMode = wasInErrorMode; - this.savedVersionId = oldSavedVersionId; - }; - } - - //#endregion - - //#region Resolve - - private lastResolvedFileStat: IFileStatWithMetadata | undefined; - - async resolve(options?: IFileWorkingCopyResolveOptions): Promise { - this.trace('[file working copy] resolve() - enter'); - - // Return early if we are disposed - if (this.isDisposed()) { - this.trace('[file working copy] resolve() - exit - without resolving because file working copy is disposed'); - - return; - } - - // Unless there are explicit contents provided, it is important that we do not - // resolve a working copy that is dirty or is in the process of saving to prevent - // data loss. - if (!options?.contents && (this.dirty || this.saveSequentializer.hasPending())) { - this.trace('[file working copy] resolve() - exit - without resolving because file working copy is dirty or being saved'); - - return; - } - - return this.doResolve(options); - } - - private async doResolve(options?: IFileWorkingCopyResolveOptions): Promise { - - // First check if we have contents to use for the working copy - if (options?.contents) { - return this.resolveFromBuffer(options.contents); - } - - // Second, check if we have a backup to resolve from (only for new working copies) - const isNew = !this.isResolved(); - if (isNew) { - const resolvedFromBackup = await this.resolveFromBackup(); - if (resolvedFromBackup) { - return; - } - } - - // Finally, resolve from file resource - return this.resolveFromFile(options); - } - - private async resolveFromBuffer(buffer: VSBufferReadableStream): Promise { - this.trace('[file working copy] resolveFromBuffer()'); - - // Try to resolve metdata from disk - let mtime: number; - let ctime: number; - let size: number; - let etag: string; - try { - const metadata = await this.fileService.resolve(this.resource, { resolveMetadata: true }); - mtime = metadata.mtime; - ctime = metadata.ctime; - size = metadata.size; - etag = metadata.etag; - - // Clear orphaned state when resolving was successful - this.setOrphaned(false); - } catch (error) { - - // Put some fallback values in error case - mtime = Date.now(); - ctime = Date.now(); - size = 0; - etag = ETAG_DISABLED; - - // Apply orphaned state based on error code - this.setOrphaned(error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND); - } - - // Resolve with buffer - return this.resolveFromContent({ - resource: this.resource, - name: this.name, - mtime, - ctime, - size, - etag, - value: buffer - }, true /* dirty (resolved from buffer) */); - } - - private async resolveFromBackup(): Promise { - - // Resolve backup if any - const backup = await this.workingCopyBackupService.resolve(this); - - // Abort if someone else managed to resolve the working copy by now - let isNew = !this.isResolved(); - if (!isNew) { - this.trace('[file working copy] resolveFromBackup() - exit - withoutresolving because previously new file working copy got created meanwhile'); - - return true; // imply that resolving has happened in another operation - } - - // Try to resolve from backup if we have any - if (backup) { - await this.doResolveFromBackup(backup); - - return true; - } - - // Otherwise signal back that resolving did not happen - return false; - } - - private async doResolveFromBackup(backup: IResolvedWorkingCopyBackup): Promise { - this.trace('[file working copy] doResolveFromBackup()'); - - // Resolve with backup - await this.resolveFromContent({ - resource: this.resource, - name: this.name, - mtime: backup.meta ? backup.meta.mtime : Date.now(), - ctime: backup.meta ? backup.meta.ctime : Date.now(), - size: backup.meta ? backup.meta.size : 0, - etag: backup.meta ? backup.meta.etag : ETAG_DISABLED, // etag disabled if unknown! - value: backup.value - }, true /* dirty (resolved from backup) */); - - // Restore orphaned flag based on state - if (backup.meta && backup.meta.orphaned) { - this.setOrphaned(true); - } - } - - private async resolveFromFile(options?: IFileWorkingCopyResolveOptions): Promise { - this.trace('[file working copy] resolveFromFile()'); - - const forceReadFromFile = options?.forceReadFromFile; - - // Decide on etag - let etag: string | undefined; - if (forceReadFromFile) { - etag = ETAG_DISABLED; // disable ETag if we enforce to read from disk - } else if (this.lastResolvedFileStat) { - etag = this.lastResolvedFileStat.etag; // otherwise respect etag to support caching - } - - // Remember current version before doing any long running operation - // to ensure we are not changing a working copy that was changed - // meanwhile - const currentVersionId = this.versionId; - - // Resolve Content - try { - const content = await this.fileService.readFileStream(this.resource, { etag }); - - // Clear orphaned state when resolving was successful - this.setOrphaned(false); - - // Return early if the working copy content has changed - // meanwhile to prevent loosing any changes - if (currentVersionId !== this.versionId) { - this.trace('[file working copy] resolveFromFile() - exit - without resolving because file working copy content changed'); - - return; - } - - await this.resolveFromContent(content, false /* not dirty (resolved from file) */); - } catch (error) { - const result = error.fileOperationResult; - - // Apply orphaned state based on error code - this.setOrphaned(result === FileOperationResult.FILE_NOT_FOUND); - - // NotModified status is expected and can be handled gracefully - // if we are resolved - if (this.isResolved() && result === FileOperationResult.FILE_NOT_MODIFIED_SINCE) { - return; - } - - // Unless we are forced to read from the file, ignore when a working copy has - // been resolved once and the file was deleted meanwhile. Since we already have - // the working copy resolved, we can return to this state and update the orphaned - // flag to indicate that this working copy has no version on disk anymore. - if (this.isResolved() && result === FileOperationResult.FILE_NOT_FOUND && !forceReadFromFile) { - return; - } - - // Otherwise bubble up the error - throw error; - } - } - - private async resolveFromContent(content: IFileStreamContent, dirty: boolean): Promise { - this.trace('[file working copy] resolveFromContent() - enter'); - - // Return early if we are disposed - if (this.isDisposed()) { - this.trace('[file working copy] resolveFromContent() - exit - because working copy is disposed'); - - return; - } - - // Update our resolved disk stat - this.updateLastResolvedFileStat({ - resource: this.resource, - name: content.name, - mtime: content.mtime, - ctime: content.ctime, - size: content.size, - etag: content.etag, - isFile: true, - isDirectory: false, - isSymbolicLink: false - }); - - // Update existing model if we had been resolved - if ((this as FileWorkingCopy).isResolved()) { // {{SQL CARBON EDIT}} Compile fixes - await this.doUpdateModel(content.value); - } - - // Create new model otherwise - else { - await this.doCreateModel(content.value); - } - - // Update working copy dirty flag. This is very important to call - // in both cases of dirty or not because it conditionally updates - // the `savedVersionId` to determine the version when to consider - // the working copy as saved again (e.g. when undoing back to the - // saved state) - this.setDirty(!!dirty); - - // Emit as event - this._onDidResolve.fire(); - } - - private async doCreateModel(contents: VSBufferReadableStream): Promise { - this.trace('[file working copy] doCreateModel()'); - - // Create model and dispose it when we get disposed - this._model = this._register(await this.modelFactory.createModel(this.resource, contents, CancellationToken.None)); - - // Model listeners - this.installModelListeners(this._model); - } - - private ignoreDirtyOnModelContentChange = false; - - private async doUpdateModel(contents: VSBufferReadableStream): Promise { - this.trace('[file working copy] doUpdateModel()'); - - // Update model value in a block that ignores content change events for dirty tracking - this.ignoreDirtyOnModelContentChange = true; - try { - await this.model?.update(contents, CancellationToken.None); - } finally { - this.ignoreDirtyOnModelContentChange = false; - } - } - - private installModelListeners(model: IFileWorkingCopyModel): void { - - // See https://github.com/microsoft/vscode/issues/30189 - // This code has been extracted to a different method because it caused a memory leak - // where `value` was captured in the content change listener closure scope. - - // Content Change - this._register(model.onDidChangeContent(e => this.onModelContentChanged(model, e.isUndoing || e.isRedoing))); - - // Lifecycle - this._register(model.onWillDispose(() => this.dispose())); - } - - private onModelContentChanged(model: IFileWorkingCopyModel, isUndoingOrRedoing: boolean): void { - this.trace(`[file working copy] onModelContentChanged() - enter`); - - // In any case increment the version id because it tracks the textual content state of the model at all times - this.versionId++; - this.trace(`[file working copy] onModelContentChanged() - new versionId ${this.versionId}`); - - // Remember when the user changed the model through a undo/redo operation. - // We need this information to throttle save participants to fix - // https://github.com/microsoft/vscode/issues/102542 - if (isUndoingOrRedoing) { - this.lastContentChangeFromUndoRedo = Date.now(); - } - - // We mark check for a dirty-state change upon model content change, unless: - // - explicitly instructed to ignore it (e.g. from model.resolve()) - // - the model is readonly (in that case we never assume the change was done by the user) - if (!this.ignoreDirtyOnModelContentChange && !this.isReadonly()) { - - // The contents changed as a matter of Undo and the version reached matches the saved one - // In this case we clear the dirty flag and emit a SAVED event to indicate this state. - if (model.versionId === this.savedVersionId) { - this.trace('[file working copy] onModelContentChanged() - model content changed back to last saved version'); - - // Clear flags - const wasDirty = this.dirty; - this.setDirty(false); - - // Emit revert event if we were dirty - if (wasDirty) { - this._onDidRevert.fire(); - } - } - - // Otherwise the content has changed and we signal this as becoming dirty - else { - this.trace('[file working copy] onModelContentChanged() - model content changed and marked as dirty'); - - // Mark as dirty - this.setDirty(true); - } - } - - // Emit as event - this._onDidChangeContent.fire(); - } - - //#endregion - - //#region Backup - - async backup(token: CancellationToken): Promise { - - // Fill in metadata if we are resolved - let meta: IFileWorkingCopyBackupMetaData | undefined = undefined; - if (this.lastResolvedFileStat) { - meta = { - mtime: this.lastResolvedFileStat.mtime, - ctime: this.lastResolvedFileStat.ctime, - size: this.lastResolvedFileStat.size, - etag: this.lastResolvedFileStat.etag, - orphaned: this.inOrphanMode - }; - } - - // Fill in content if we are resolved - let content: VSBufferReadableStream | undefined = undefined; - if (this.isResolved()) { - content = await raceCancellation(this.model.snapshot(token), token); - } - - return { meta, content }; - } - - //#endregion - - //#region Save - - private versionId = 0; - - private static readonly UNDO_REDO_SAVE_PARTICIPANTS_AUTO_SAVE_THROTTLE_THRESHOLD = 500; - private lastContentChangeFromUndoRedo: number | undefined = undefined; - - private readonly saveSequentializer = new TaskSequentializer(); - - async save(options: IFileWorkingCopySaveOptions = Object.create(null)): Promise { - if (!this.isResolved()) { - return false; - } - - if (this.isReadonly()) { - this.trace('[file working copy] save() - ignoring request for readonly resource'); - - return false; // if working copy is readonly we do not attempt to save at all - } - - if ( - (this.hasState(FileWorkingCopyState.CONFLICT) || this.hasState(FileWorkingCopyState.ERROR)) && - (options.reason === SaveReason.AUTO || options.reason === SaveReason.FOCUS_CHANGE || options.reason === SaveReason.WINDOW_CHANGE) - ) { - this.trace('[file working copy] save() - ignoring auto save request for file working copy that is in conflict or error'); - - return false; // if working copy is in save conflict or error, do not save unless save reason is explicit - } - - // Actually do save - this.trace('[file working copy] save() - enter'); - await this.doSave(options); - this.trace('[file working copy] save() - exit'); - - return true; - } - - private async doSave(options: IFileWorkingCopySaveOptions): Promise { - if (typeof options.reason !== 'number') { - options.reason = SaveReason.EXPLICIT; - } - - let versionId = this.versionId; - this.trace(`[file working copy] doSave(${versionId}) - enter with versionId ${versionId}`); - - // Lookup any running pending save for this versionId and return it if found - // - // Scenario: user invoked the save action multiple times quickly for the same contents - // while the save was not yet finished to disk - // - if ((this.saveSequentializer as TaskSequentializer).hasPending(versionId)) { // {{SQL CARBON EDIT}} Compile fixes - this.trace(`[file working copy] doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`); - - return this.saveSequentializer.pending; - } - - // Return early if not dirty (unless forced) - // - // Scenario: user invoked save action even though the working copy is not dirty - if (!options.force && !this.dirty) { - this.trace(`[file working copy] doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`); - - return; - } - - // Return if currently saving by storing this save request as the next save that should happen. - // Never ever must 2 saves execute at the same time because this can lead to dirty writes and race conditions. - // - // Scenario A: auto save was triggered and is currently busy saving to disk. this takes long enough that another auto save - // kicks in. - // Scenario B: save is very slow (e.g. network share) and the user manages to change the working copy and trigger another save - // while the first save has not returned yet. - // - if ((this.saveSequentializer as TaskSequentializer).hasPending()) { // {{SQL CARBON EDIT}} Compile fixes - this.trace(`[file working copy] doSave(${versionId}) - exit - because busy saving`); - - // Indicate to the save sequentializer that we want to - // cancel the pending operation so that ours can run - // before the pending one finishes. - // Currently this will try to cancel pending save - // participants and pending snapshots from the - // save operation, but not the actual save which does - // not support cancellation yet. - this.saveSequentializer.cancelPending(); - - // Register this as the next upcoming save and return - return this.saveSequentializer.setNext(() => this.doSave(options)); - } - - // Push all edit operations to the undo stack so that the user has a chance to - // Ctrl+Z back to the saved version. - if (this.isResolved()) { - this.model.pushStackElement(); - } - - const saveCancellation = new CancellationTokenSource(); - - return this.saveSequentializer.setPending(versionId, (async () => { - - // A save participant can still change the working copy now - // and since we are so close to saving we do not want to trigger - // another auto save or similar, so we block this - // In addition we update our version right after in case it changed - // because of a working copy change - // Save participants can also be skipped through API. - if (this.isResolved() && !options.skipSaveParticipants && this.isTextFileModel(this.model)) { - try { - - // Measure the time it took from the last undo/redo operation to this save. If this - // time is below `UNDO_REDO_SAVE_PARTICIPANTS_THROTTLE_THRESHOLD`, we make sure to - // delay the save participant for the remaining time if the reason is auto save. - // - // This fixes the following issue: - // - the user has configured auto save with delay of 100ms or shorter - // - the user has a save participant enabled that modifies the file on each save - // - the user types into the file and the file gets saved - // - the user triggers undo operation - // - this will undo the save participant change but trigger the save participant right after - // - the user has no chance to undo over the save participant - // - // Reported as: https://github.com/microsoft/vscode/issues/102542 - if (options.reason === SaveReason.AUTO && typeof this.lastContentChangeFromUndoRedo === 'number') { - const timeFromUndoRedoToSave = Date.now() - this.lastContentChangeFromUndoRedo; - if (timeFromUndoRedoToSave < FileWorkingCopy.UNDO_REDO_SAVE_PARTICIPANTS_AUTO_SAVE_THROTTLE_THRESHOLD) { - await timeout(FileWorkingCopy.UNDO_REDO_SAVE_PARTICIPANTS_AUTO_SAVE_THROTTLE_THRESHOLD - timeFromUndoRedoToSave); - } - } - - // Run save participants unless save was cancelled meanwhile - if (!saveCancellation.token.isCancellationRequested) { - await this.textFileService.files.runSaveParticipants(this.model, { reason: options.reason ?? SaveReason.EXPLICIT }, saveCancellation.token); - } - } catch (error) { - this.logService.error(`[file working copy] runSaveParticipants(${versionId}) - resulted in an error: ${error.toString()}`, this.resource.toString(true), this.typeId); - } - } - - // It is possible that a subsequent save is cancelling this - // running save. As such we return early when we detect that. - if (saveCancellation.token.isCancellationRequested) { - return; - } - - // We have to protect against being disposed at this point. It could be that the save() operation - // was triggerd followed by a dispose() operation right after without waiting. Typically we cannot - // be disposed if we are dirty, but if we are not dirty, save() and dispose() can still be triggered - // one after the other without waiting for the save() to complete. If we are disposed(), we risk - // saving contents to disk that are stale (see https://github.com/microsoft/vscode/issues/50942). - // To fix this issue, we will not store the contents to disk when we got disposed. - if (this.isDisposed()) { - return; - } - - // We require a resolved working copy from this point on, since we are about to write data to disk. - if (!this.isResolved()) { - return; - } - - // update versionId with its new value (if pre-save changes happened) - versionId = this.versionId; - - // Clear error flag since we are trying to save again - this.inErrorMode = false; - - // Save to Disk. We mark the save operation as currently pending with - // the latest versionId because it might have changed from a save - // participant triggering - this.trace(`[file working copy] doSave(${versionId}) - before write()`); - const lastResolvedFileStat = assertIsDefined(this.lastResolvedFileStat); - const resolvedFileWorkingCopy = this; - return this.saveSequentializer.setPending(versionId, (async () => { - try { - - // Snapshot working copy model contents - const snapshot = await raceCancellation(resolvedFileWorkingCopy.model.snapshot(saveCancellation.token), saveCancellation.token); - - // It is possible that a subsequent save is cancelling this - // running save. As such we return early when we detect that - // However, we do not pass the token into the file service - // because that is an atomic operation currently without - // cancellation support, so we dispose the cancellation if - // it was not cancelled yet. - if (saveCancellation.token.isCancellationRequested) { - return; - } else { - saveCancellation.dispose(); - } - - const writeFileOptions: IWriteFileOptions = { - mtime: lastResolvedFileStat.mtime, - etag: (options.ignoreModifiedSince || !this.filesConfigurationService.preventSaveConflicts(lastResolvedFileStat.resource)) ? ETAG_DISABLED : lastResolvedFileStat.etag, - unlock: options.writeUnlock - }; - - // Write them to disk - let stat: IFileStatWithMetadata; - if (options?.writeElevated && this.elevatedFileService.isSupported(lastResolvedFileStat.resource)) { - stat = await this.elevatedFileService.writeFileElevated(lastResolvedFileStat.resource, assertIsDefined(snapshot), writeFileOptions); - } else { - stat = await this.fileService.writeFile(lastResolvedFileStat.resource, assertIsDefined(snapshot), writeFileOptions); - } - - this.handleSaveSuccess(stat, versionId, options); - } catch (error) { - this.handleSaveError(error, versionId, options); - } - })(), () => saveCancellation.cancel()); - })(), () => saveCancellation.cancel()); - } - - private handleSaveSuccess(stat: IFileStatWithMetadata, versionId: number, options: IFileWorkingCopySaveOptions): void { - - // Updated resolved stat with updated stat - this.updateLastResolvedFileStat(stat); - - // Update dirty state unless working copy has changed meanwhile - if (versionId === this.versionId) { - this.trace(`[file working copy] handleSaveSuccess(${versionId}) - setting dirty to false because versionId did not change`); - this.setDirty(false); - } else { - this.trace(`[file working copy] handleSaveSuccess(${versionId}) - not setting dirty to false because versionId did change meanwhile`); - } - - // Update orphan state given save was successful - this.setOrphaned(false); - - // Emit Save Event - this._onDidSave.fire(options.reason ?? SaveReason.EXPLICIT); - } - - private handleSaveError(error: Error, versionId: number, options: IFileWorkingCopySaveOptions): void { - this.logService.error(`[file working copy] handleSaveError(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource.toString(true), this.typeId); - - // Return early if the save() call was made asking to - // handle the save error itself. - if (options.ignoreErrorHandler) { - throw error; - } - - // In any case of an error, we mark the working copy as dirty to prevent data loss - // It could be possible that the write corrupted the file on disk (e.g. when - // an error happened after truncating the file) and as such we want to preserve - // the working copy contents to prevent data loss. - this.setDirty(true); - - // Flag as error state - this.inErrorMode = true; - - // Look out for a save conflict - if ((error as FileOperationError).fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) { - this.inConflictMode = true; - } - - // Delegate to save error handler - if (this.isTextFileModel(this.model)) { - this.textFileService.files.saveErrorHandler.onSaveError(error, this.model); - } else { - this.doHandleSaveError(error); - } - - // Emit as event - this._onDidSaveError.fire(); - } - - private doHandleSaveError(error: Error): void { - const fileOperationError = error as FileOperationError; - const primaryActions: IAction[] = []; - - let message: string; - - // Dirty write prevention - if (fileOperationError.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) { - message = localize('staleSaveError', "Failed to save '{0}': The content of the file is newer. Do you want to overwrite the file with your changes?", this.name); - - primaryActions.push(toAction({ id: 'fileWorkingCopy.overwrite', label: localize('overwrite', "Overwrite"), run: () => this.save({ ignoreModifiedSince: true }) })); - primaryActions.push(toAction({ id: 'fileWorkingCopy.revert', label: localize('discard', "Discard"), run: () => this.revert() })); - } - - // Any other save error - else { - const isWriteLocked = fileOperationError.fileOperationResult === FileOperationResult.FILE_WRITE_LOCKED; - const triedToUnlock = isWriteLocked && fileOperationError.options?.unlock; - const isPermissionDenied = fileOperationError.fileOperationResult === FileOperationResult.FILE_PERMISSION_DENIED; - const canSaveElevated = this.elevatedFileService.isSupported(this.resource); - - // Save Elevated - if (canSaveElevated && (isPermissionDenied || triedToUnlock)) { - primaryActions.push(toAction({ - id: 'fileWorkingCopy.saveElevated', - label: triedToUnlock ? - isWindows ? localize('overwriteElevated', "Overwrite as Admin...") : localize('overwriteElevatedSudo', "Overwrite as Sudo...") : - isWindows ? localize('saveElevated', "Retry as Admin...") : localize('saveElevatedSudo', "Retry as Sudo..."), - run: () => { - this.save({ writeElevated: true, writeUnlock: triedToUnlock, reason: SaveReason.EXPLICIT }); - } - })); - } - - // Unlock - else if (isWriteLocked) { - primaryActions.push(toAction({ id: 'fileWorkingCopy.unlock', label: localize('overwrite', "Overwrite"), run: () => this.save({ writeUnlock: true, reason: SaveReason.EXPLICIT }) })); - } - - // Retry - else { - primaryActions.push(toAction({ id: 'fileWorkingCopy.retry', label: localize('retry', "Retry"), run: () => this.save({ reason: SaveReason.EXPLICIT }) })); - } - - // Save As - primaryActions.push(toAction({ - id: 'fileWorkingCopy.saveAs', - label: localize('saveAs', "Save As..."), - run: () => { - const editor = this.workingCopyEditorService.findEditor(this); - if (editor) { - this.editorService.save(editor, { saveAs: true, reason: SaveReason.EXPLICIT }); - } - } - })); - - // Discard - primaryActions.push(toAction({ id: 'fileWorkingCopy.revert', label: localize('discard', "Discard"), run: () => this.revert() })); - - // Message - if (isWriteLocked) { - if (triedToUnlock && canSaveElevated) { - message = isWindows ? - localize('readonlySaveErrorAdmin', "Failed to save '{0}': File is read-only. Select 'Overwrite as Admin' to retry as administrator.", this.name) : - localize('readonlySaveErrorSudo', "Failed to save '{0}': File is read-only. Select 'Overwrite as Sudo' to retry as superuser.", this.name); - } else { - message = localize('readonlySaveError', "Failed to save '{0}': File is read-only. Select 'Overwrite' to attempt to make it writeable.", this.name); - } - } else if (canSaveElevated && isPermissionDenied) { - message = isWindows ? - localize('permissionDeniedSaveError', "Failed to save '{0}': Insufficient permissions. Select 'Retry as Admin' to retry as administrator.", this.name) : - localize('permissionDeniedSaveErrorSudo', "Failed to save '{0}': Insufficient permissions. Select 'Retry as Sudo' to retry as superuser.", this.name); - } else { - message = localize({ key: 'genericSaveError', comment: ['{0} is the resource that failed to save and {1} the error message'] }, "Failed to save '{0}': {1}", this.name, toErrorMessage(error, false)); - } - } - - // Show to the user as notification - const handle = this.notificationService.notify({ id: `${hash(this.resource.toString())}`, severity: Severity.Error, message, actions: { primary: primaryActions } }); - - // Remove automatically when we get saved/reverted - const listener = Event.once(Event.any(this.onDidSave, this.onDidRevert))(() => handle.close()); - Event.once(handle.onDidClose)(() => listener.dispose()); - } - - private updateLastResolvedFileStat(newFileStat: IFileStatWithMetadata): void { - - // First resolve - just take - if (!this.lastResolvedFileStat) { - this.lastResolvedFileStat = newFileStat; - } - - // Subsequent resolve - make sure that we only assign it if the mtime - // is equal or has advanced. - // This prevents race conditions from resolving and saving. If a save - // comes in late after a revert was called, the mtime could be out of - // sync. - else if (this.lastResolvedFileStat.mtime <= newFileStat.mtime) { - this.lastResolvedFileStat = newFileStat; - } - } - - //#endregion - - //#region Revert - - async revert(options?: IRevertOptions): Promise { - if (!this.isResolved() || (!this.dirty && !options?.force)) { - return; // ignore if not resolved or not dirty and not enforced - } - - // Unset flags - const wasDirty = this.dirty; - const undoSetDirty = this.doSetDirty(false); - - // Force read from disk unless reverting soft - const softUndo = options?.soft; - if (!softUndo) { - try { - await this.resolve({ forceReadFromFile: true }); - } catch (error) { - - // FileNotFound means the file got deleted meanwhile, so ignore it - if ((error as FileOperationError).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) { - - // Set flags back to previous values, we are still dirty if revert failed - undoSetDirty(); - - throw error; - } - } - } - - // Emit file change event - this._onDidRevert.fire(); - - // Emit dirty change event - if (wasDirty) { - this._onDidChangeDirty.fire(); - } - } - - //#endregion - - //#region State - - private inConflictMode = false; - private inErrorMode = false; - - hasState(state: FileWorkingCopyState): boolean { - switch (state) { - case FileWorkingCopyState.CONFLICT: - return this.inConflictMode; - case FileWorkingCopyState.DIRTY: - return this.dirty; - case FileWorkingCopyState.ERROR: - return this.inErrorMode; - case FileWorkingCopyState.ORPHAN: - return this.inOrphanMode; - case FileWorkingCopyState.PENDING_SAVE: - return this.saveSequentializer.hasPending(); - case FileWorkingCopyState.SAVED: - return !this.dirty; - } - } - - joinState(state: FileWorkingCopyState.PENDING_SAVE): Promise { - return this.saveSequentializer.pending ?? Promise.resolve(); - } - - //#endregion - - //#region Utilities - - isResolved(): this is IResolvedFileWorkingCopy { - return !!this.model; - } - - isReadonly(): boolean { - return this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); - } - - private trace(msg: string): void { - this.logService.trace(msg, this.resource.toString(true), this.typeId); - } - - //#endregion - - //#region Dispose - - private disposed = false; - - isDisposed(): boolean { - return this.disposed; - } - - override dispose(): void { - this.trace('[file working copy] dispose()'); - - // State - this.disposed = true; - this.inConflictMode = false; - this.inOrphanMode = false; - this.inErrorMode = false; - - // Event - this._onWillDispose.fire(); - - super.dispose(); - } - - //#endregion - - //#region Remainders of text file model world (TODO@bpasero callers have to be handled in a generic way) - - private isTextFileModel(model: unknown): model is ITextFileEditorModel { - const textFileModel = this.textFileService.files.get(this.resource); - - return !!(textFileModel && this.model && (textFileModel as unknown) === (this.model as unknown)); - } - - //#endregion + readonly model: M; } diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index 8e13578781..2f4b0d772d 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -1,93 +1,98 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { Event, Emitter } from 'vs/base/common/event'; -import { FileWorkingCopy, FileWorkingCopyState, IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory, IFileWorkingCopySaveOptions } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; -import { SaveReason } from 'vs/workbench/common/editor'; -import { ResourceMap } from 'vs/base/common/map'; -import { Promises, ResourceQueue } from 'vs/base/common/async'; -import { FileChangesEvent, FileChangeType, FileOperation, IFileService } from 'vs/platform/files/common/files'; -import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { Event } from 'vs/base/common/event'; +import { Promises } from 'vs/base/common/async'; import { VSBufferReadableStream } from 'vs/base/common/buffer'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ILogService } from 'vs/platform/log/common/log'; -import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { joinPath } from 'vs/base/common/resources'; -import { IWorkingCopyFileService, WorkingCopyFileEvent } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; -import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { toLocalResource, joinPath, isEqual, basename, dirname } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { IFileDialogService, IDialogService, IConfirmation } from 'vs/platform/dialogs/common/dialogs'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ISaveOptions } from 'vs/workbench/common/editor'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelFactory, IStoredFileWorkingCopyResolveOptions } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; +import { StoredFileWorkingCopyManager, IStoredFileWorkingCopyManager, IStoredFileWorkingCopyManagerResolveOptions } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager'; +import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelFactory, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; +import { INewOrExistingUntitledFileWorkingCopyOptions, INewUntitledFileWorkingCopyOptions, INewUntitledFileWorkingCopyWithAssociatedResourceOptions, IUntitledFileWorkingCopyManager, UntitledFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager'; +import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { isValidBasename } from 'vs/base/common/extpath'; +import { IBaseFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; +import { IFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { ILogService } from 'vs/platform/log/common/log'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IElevatedFileService } from 'vs/workbench/services/files/common/elevatedFileService'; +import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -/** - * The only one that should be dealing with `IFileWorkingCopy` and handle all - * operations that are working copy related, such as save/revert, backup - * and resolving. - */ -export interface IFileWorkingCopyManager extends IDisposable { +export interface IFileWorkingCopyManager extends IBaseFileWorkingCopyManager> { /** - * An event for when a file working copy was created. + * Provides access to the manager for stored file working copies. */ - readonly onDidCreate: Event>; + readonly stored: IStoredFileWorkingCopyManager; /** - * An event for when a file working copy was resolved. + * Provides access to the manager for untitled file working copies. */ - readonly onDidResolve: Event>; + readonly untitled: IUntitledFileWorkingCopyManager; /** - * An event for when a file working copy changed it's dirty state. - */ - readonly onDidChangeDirty: Event>; - - /** - * An event for when a file working copy failed to save. - */ - readonly onDidSaveError: Event>; - - /** - * An event for when a file working copy successfully saved. - */ - readonly onDidSave: Event>; - - /** - * An event for when a file working copy was reverted. - */ - readonly onDidRevert: Event>; - - /** - * Access to all known file working copies within the manager. - */ - readonly workingCopies: readonly IFileWorkingCopy[]; - - /** - * Returns the file working copy for the provided resource - * or `undefined` if none. - */ - get(resource: URI): IFileWorkingCopy | undefined; - - /** - * Allows to resolve a file working copy. If the manager already knows - * about a file working copy with the same `URI`, it will return that - * existing file working copy. There will never be more than one - * file working copy per `URI` until the file working copy is disposed. + * Allows to resolve a stored file working copy. If the manager already knows + * about a stored file working copy with the same `URI`, it will return that + * existing stored file working copy. There will never be more than one + * stored file working copy per `URI` until the stored file working copy is + * disposed. * - * Use the `IFileWorkingCopyResolveOptions.reload` option to control the - * behaviour for when a file working copy was previously already resolved + * Use the `IStoredFileWorkingCopyResolveOptions.reload` option to control the + * behaviour for when a stored file working copy was previously already resolved * with regards to resolving it again from the underlying file resource * or not. * * Note: Callers must `dispose` the working copy when no longer needed. * - * @param resource used as unique identifier of the file working copy in + * @param resource used as unique identifier of the stored file working copy in * case one is already known for this `URI`. * @param options */ - resolve(resource: URI, options?: IFileWorkingCopyResolveOptions): Promise>; + resolve(resource: URI, options?: IStoredFileWorkingCopyManagerResolveOptions): Promise>; + + /** + * Create a new untitled file working copy with optional initial contents. + * + * Note: Callers must `dispose` the working copy when no longer needed. + */ + resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; + + /** + * Create a new untitled file working copy with optional initial contents + * and associated resource. The associated resource will be used when + * saving and will not require to ask the user for a file path. + * + * Note: Callers must `dispose` the working copy when no longer needed. + */ + resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; + + /** + * Creates a new untitled file working copy with optional initial contents + * with the provided resource or return an existing untitled file working + * copy otherwise. + * + * Note: Callers must `dispose` the working copy when no longer needed. + */ + resolve(options?: INewOrExistingUntitledFileWorkingCopyOptions): Promise>; /** * Implements "Save As" for file based working copies. The API is `URI` based @@ -103,62 +108,19 @@ export interface IFileWorkingCopyManager extend * * Note: Callers must `dispose` the working copy when no longer needed. * + * Note: Untitled file working copies are being disposed when saved. + * * @param source the source resource to save as * @param target the optional target resource to save to. if not defined, the user * will be asked for input - * @returns the target working copy that was saved to or `undefined` in case of + * @returns the target stored working copy that was saved to or `undefined` in case of * cancellation */ - saveAs(source: URI, target: URI, options?: IFileWorkingCopySaveOptions): Promise | undefined>; - saveAs(source: URI, target: undefined, options?: IFileWorkingCopySaveAsOptions): Promise | undefined>; - - /** - * Waits for the file working copy to be ready to be disposed. There may be - * conditions under which the file working copy cannot be disposed, e.g. when - * it is dirty. Once the promise is settled, it is safe to dispose. - */ - canDispose(workingCopy: IFileWorkingCopy): true | Promise; + saveAs(source: URI, target: URI, options?: ISaveOptions): Promise | undefined>; + saveAs(source: URI, target: undefined, options?: IFileWorkingCopySaveAsOptions): Promise | undefined>; } -export interface IFileWorkingCopySaveEvent { - - /** - * The file working copy that was successfully saved. - */ - workingCopy: IFileWorkingCopy; - - /** - * The reason why the file working copy was saved. - */ - reason: SaveReason; -} - -export interface IFileWorkingCopyResolveOptions { - - /** - * The contents to use for the file working copy if known. - * If not provided, the contents will be retrieved from the - * underlying resource or backup if present. - * - * If contents are provided, the file working copy will be marked - * as dirty right from the beginning. - */ - contents?: VSBufferReadableStream; - - /** - * If the file working copy was already resolved before, - * allows to trigger a reload of it to fetch the latest contents: - * - async: resolve() will return immediately and trigger - * a reload that will run in the background. - * - sync: resolve() will only return resolved when the - * file working copy has finished reloading. - */ - reload?: { - async: boolean - }; -} - -export interface IFileWorkingCopySaveAsOptions extends IFileWorkingCopySaveOptions { +export interface IFileWorkingCopySaveAsOptions extends ISaveOptions { /** * Optional target resource to suggest to the user in case @@ -167,441 +129,151 @@ export interface IFileWorkingCopySaveAsOptions extends IFileWorkingCopySaveOptio suggestedTarget?: URI; } -export class FileWorkingCopyManager extends Disposable implements IFileWorkingCopyManager { +export class FileWorkingCopyManager extends Disposable implements IFileWorkingCopyManager { - //#region Events + readonly onDidCreate: Event>; - private readonly _onDidCreate = this._register(new Emitter>()); - readonly onDidCreate = this._onDidCreate.event; - - private readonly _onDidResolve = this._register(new Emitter>()); - readonly onDidResolve = this._onDidResolve.event; - - private readonly _onDidChangeDirty = this._register(new Emitter>()); - readonly onDidChangeDirty = this._onDidChangeDirty.event; - - private readonly _onDidSaveError = this._register(new Emitter>()); - readonly onDidSaveError = this._onDidSaveError.event; - - private readonly _onDidSave = this._register(new Emitter>()); - readonly onDidSave = this._onDidSave.event; - - private readonly _onDidRevert = this._register(new Emitter>()); - readonly onDidRevert = this._onDidRevert.event; - - //#endregion - - private readonly mapResourceToWorkingCopy = new ResourceMap>(); - private readonly mapResourceToWorkingCopyListeners = new ResourceMap(); - private readonly mapResourceToDisposeListener = new ResourceMap(); - private readonly mapResourceToPendingWorkingCopyResolve = new ResourceMap>(); - - private readonly workingCopyResolveQueue = this._register(new ResourceQueue()); + readonly stored: IStoredFileWorkingCopyManager; + readonly untitled: IUntitledFileWorkingCopyManager; constructor( private readonly workingCopyTypeId: string, - private readonly modelFactory: IFileWorkingCopyModelFactory, + private readonly storedWorkingCopyModelFactory: IStoredFileWorkingCopyModelFactory, + private readonly untitledWorkingCopyModelFactory: IUntitledFileWorkingCopyModelFactory, @IFileService private readonly fileService: IFileService, - @ILifecycleService private readonly lifecycleService: ILifecycleService, - @ILabelService private readonly labelService: ILabelService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @ILogService private readonly logService: ILogService, - @IFileDialogService private readonly fileDialogService: IFileDialogService, + @ILifecycleService lifecycleService: ILifecycleService, + @ILabelService labelService: ILabelService, + @ILogService logService: ILogService, @IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService + @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IFileDialogService private readonly fileDialogService: IFileDialogService, + @ITextFileService textFileService: ITextFileService, + @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, + @IWorkingCopyService workingCopyService: IWorkingCopyService, + @INotificationService notificationService: INotificationService, + @IWorkingCopyEditorService workingCopyEditorService: IWorkingCopyEditorService, + @IEditorService editorService: IEditorService, + @IElevatedFileService elevatedFileService: IElevatedFileService, + @IPathService private readonly pathService: IPathService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IDialogService private readonly dialogService: IDialogService ) { super(); - this.registerListeners(); + // Stored file working copies manager + this.stored = this._register(new StoredFileWorkingCopyManager( + this.workingCopyTypeId, + this.storedWorkingCopyModelFactory, + fileService, lifecycleService, labelService, logService, workingCopyFileService, + workingCopyBackupService, uriIdentityService, textFileService, filesConfigurationService, + workingCopyService, notificationService, workingCopyEditorService, editorService, elevatedFileService + )); + + // Untitled file working copies manager + this.untitled = this._register(new UntitledFileWorkingCopyManager( + this.workingCopyTypeId, + this.untitledWorkingCopyModelFactory, + async (workingCopy, options) => { + const result = await this.saveAs(workingCopy.resource, undefined, options); + + return result ? true : false; + }, + fileService, labelService, logService, workingCopyBackupService, workingCopyService + )); + + // Events + this.onDidCreate = Event.any>(this.stored.onDidCreate, this.untitled.onDidCreate); } - private registerListeners(): void { + //#region get / get all - // Update working copies from file change events - this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e))); - - // Working copy operations - this._register(this.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => this.onWillRunWorkingCopyFileOperation(e))); - this._register(this.workingCopyFileService.onDidFailWorkingCopyFileOperation(e => this.onDidFailWorkingCopyFileOperation(e))); - this._register(this.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => this.onDidRunWorkingCopyFileOperation(e))); - - // Lifecycle - this.lifecycleService.onWillShutdown(event => event.join(this.onWillShutdown(), 'join.fileWorkingCopyManager')); - this.lifecycleService.onDidShutdown(() => this.dispose()); + get workingCopies(): (IUntitledFileWorkingCopy | IStoredFileWorkingCopy)[] { + return [...this.stored.workingCopies, ...this.untitled.workingCopies]; } - private async onWillShutdown(): Promise { - let fileWorkingCopies: IFileWorkingCopy[]; - - // As long as file working copies are pending to be saved, we prolong the shutdown - // until that has happened to ensure we are not shutting down in the middle of - // writing to the working copy (https://github.com/microsoft/vscode/issues/116600). - while ((fileWorkingCopies = this.workingCopies.filter(workingCopy => workingCopy.hasState(FileWorkingCopyState.PENDING_SAVE))).length > 0) { - await Promises.settled(fileWorkingCopies.map(workingCopy => workingCopy.joinState(FileWorkingCopyState.PENDING_SAVE))); - } - } - - //#region Resolve from file changes - - private onDidFilesChange(e: FileChangesEvent): void { - for (const workingCopy of this.workingCopies) { - if (workingCopy.isDirty() || !workingCopy.isResolved()) { - continue; // require a resolved, saved working copy to continue - } - - // Trigger a resolve for any update or add event that impacts - // the working copy. We also consider the added event - // because it could be that a file was added and updated - // right after. - if (e.contains(workingCopy.resource, FileChangeType.UPDATED, FileChangeType.ADDED)) { - this.queueWorkingCopyResolve(workingCopy); - } - } - } - - private queueWorkingCopyResolve(workingCopy: IFileWorkingCopy): void { - - // Resolves a working copy to update (use a queue to prevent accumulation of - // resolve when the resolving actually takes long. At most we only want the - // queue to have a size of 2 (1 running resolve and 1 queued resolve). - const queue = this.workingCopyResolveQueue.queueFor(workingCopy.resource); - if (queue.size <= 1) { - queue.queue(async () => { - try { - await workingCopy.resolve(); - } catch (error) { - this.logService.error(error); - } - }); - } + get(resource: URI): IUntitledFileWorkingCopy | IStoredFileWorkingCopy | undefined { + return this.stored.get(resource) ?? this.untitled.get(resource); } //#endregion - //#region Working Copy File Events + //#region resolve - private readonly mapCorrelationIdToWorkingCopiesToRestore = new Map(); - - private onWillRunWorkingCopyFileOperation(e: WorkingCopyFileEvent): void { - - // Move / Copy: remember working copies to restore after the operation - if (e.operation === FileOperation.MOVE || e.operation === FileOperation.COPY) { - e.waitUntil((async () => { - const workingCopiesToRestore: { source: URI, target: URI, snapshot?: VSBufferReadableStream; }[] = []; - - for (const { source, target } of e.files) { - if (source) { - if (this.uriIdentityService.extUri.isEqual(source, target)) { - continue; // ignore if resources are considered equal - } - - // Find all working copies that related to source (can be many if resource is a folder) - const sourceWorkingCopies: IFileWorkingCopy[] = []; - for (const workingCopy of this.workingCopies) { - if (this.uriIdentityService.extUri.isEqualOrParent(workingCopy.resource, source)) { - sourceWorkingCopies.push(workingCopy); - } - } - - // Remember each source working copy to load again after move is done - // with optional content to restore if it was dirty - for (const sourceWorkingCopy of sourceWorkingCopies) { - const sourceResource = sourceWorkingCopy.resource; - - // If the source is the actual working copy, just use target as new resource - let targetResource: URI; - if (this.uriIdentityService.extUri.isEqual(sourceResource, source)) { - targetResource = target; - } - - // Otherwise a parent folder of the source is being moved, so we need - // to compute the target resource based on that - else { - targetResource = joinPath(target, sourceResource.path.substr(source.path.length + 1)); - } - - workingCopiesToRestore.push({ - source: sourceResource, - target: targetResource, - snapshot: sourceWorkingCopy.isDirty() ? await sourceWorkingCopy.model?.snapshot(CancellationToken.None) : undefined - }); - } - } - } - - this.mapCorrelationIdToWorkingCopiesToRestore.set(e.correlationId, workingCopiesToRestore); - })()); + resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; + resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; + resolve(options?: INewOrExistingUntitledFileWorkingCopyOptions): Promise>; + resolve(resource: URI, options?: IStoredFileWorkingCopyResolveOptions): Promise>; + resolve(arg1?: URI | INewUntitledFileWorkingCopyOptions | INewUntitledFileWorkingCopyWithAssociatedResourceOptions | INewOrExistingUntitledFileWorkingCopyOptions, arg2?: IStoredFileWorkingCopyResolveOptions): Promise | IStoredFileWorkingCopy> { + if (URI.isUri(arg1)) { + return this.stored.resolve(arg1, arg2); } - } - private onDidFailWorkingCopyFileOperation(e: WorkingCopyFileEvent): void { - - // Move / Copy: restore dirty flag on working copies to restore that were dirty - if ((e.operation === FileOperation.MOVE || e.operation === FileOperation.COPY)) { - const workingCopiesToRestore = this.mapCorrelationIdToWorkingCopiesToRestore.get(e.correlationId); - if (workingCopiesToRestore) { - this.mapCorrelationIdToWorkingCopiesToRestore.delete(e.correlationId); - - workingCopiesToRestore.forEach(workingCopy => { - - // Snapshot presence means this working copy used to be dirty and so we restore that - // flag. we do NOT have to restore the content because the working copy was only soft - // reverted and did not loose its original dirty contents. - if (workingCopy.snapshot) { - this.get(workingCopy.source)?.markDirty(); - } - }); - } - } - } - - private onDidRunWorkingCopyFileOperation(e: WorkingCopyFileEvent): void { - switch (e.operation) { - - // Create: Revert existing working copies - case FileOperation.CREATE: - e.waitUntil((async () => { - for (const { target } of e.files) { - const workingCopy = this.get(target); - if (workingCopy && !workingCopy.isDisposed()) { - await workingCopy.revert(); - } - } - })()); - break; - - // Move/Copy: restore working copies that were loaded before the operation took place - case FileOperation.MOVE: - case FileOperation.COPY: - e.waitUntil((async () => { - const workingCopiesToRestore = this.mapCorrelationIdToWorkingCopiesToRestore.get(e.correlationId); - if (workingCopiesToRestore) { - this.mapCorrelationIdToWorkingCopiesToRestore.delete(e.correlationId); - - await Promises.settled(workingCopiesToRestore.map(async workingCopyToRestore => { - - // Restore the working copy at the target. if we have previous dirty content, we pass it - // over to be used, otherwise we force a reload from disk. this is important - // because we know the file has changed on disk after the move and the working copy might - // have still existed with the previous state. this ensures that the working copy is not - // tracking a stale state. - await this.resolve(workingCopyToRestore.target, { - reload: { async: false }, // enforce a reload - contents: workingCopyToRestore.snapshot - }); - })); - } - })()); - break; - } + return this.untitled.resolve(arg1); } //#endregion - //#region Get / Get all + //#region Save - get workingCopies(): IFileWorkingCopy[] { - return [...this.mapResourceToWorkingCopy.values()]; + async saveAs(source: URI, target?: URI, options?: IFileWorkingCopySaveAsOptions): Promise | undefined> { + + // Get to target resource + if (!target) { + const workingCopy = this.get(source); + if (workingCopy instanceof UntitledFileWorkingCopy && workingCopy.hasAssociatedFilePath) { + target = await this.suggestSavePath(source); + } else { + target = await this.fileDialogService.pickFileToSave(await this.suggestSavePath(options?.suggestedTarget ?? source), options?.availableFileSystems); + } + } + + if (!target) { + return undefined; // user canceled {{SQL CARBON EDIT}} Strict nulls + } + + // Just save if target is same as working copies own resource + // and we are not saving an untitled file working copy + if (this.fileService.canHandleResource(source) && isEqual(source, target)) { + return this.doSave(source, { ...options, force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ }); + } + + // If the target is different but of same identity, we + // move the source to the target, knowing that the + // underlying file system cannot have both and then save. + // However, this will only work if the source exists + // and is not orphaned, so we need to check that too. + if (this.fileService.canHandleResource(source) && this.uriIdentityService.extUri.isEqual(source, target) && (await this.fileService.exists(source))) { + + // Move via working copy file service to enable participants + await this.workingCopyFileService.move([{ file: { source, target } }], CancellationToken.None); + + // At this point we don't know whether we have a + // working copy for the source or the target URI so we + // simply try to save with both resources. + return (await this.doSave(source, options)) ?? (await this.doSave(target, options)); + } + + // Perform normal "Save As" + return this.doSaveAs(source, target, options); } - get(resource: URI): IFileWorkingCopy | undefined { - return this.mapResourceToWorkingCopy.get(resource); - } + private async doSave(resource: URI, options?: ISaveOptions): Promise | undefined> { - //#endregion - - //#region Resolve - - async resolve(resource: URI, options?: IFileWorkingCopyResolveOptions): Promise> { - - // Await a pending working copy resolve first before proceeding - // to ensure that we never resolve a working copy more than once - // in parallel - const pendingResolve = this.joinPendingResolve(resource); - if (pendingResolve) { - await pendingResolve; - } - - let workingCopyResolve: Promise; - let workingCopy = this.get(resource); - let didCreateWorkingCopy = false; - - // Working copy exists - if (workingCopy) { - - // Always reload if contents are provided - if (options?.contents) { - workingCopyResolve = workingCopy.resolve(options); + // Save is only possible with stored file working copies, + // any other have to go via `saveAs` flow. + const storedFileWorkingCopy = this.stored.get(resource); + if (storedFileWorkingCopy) { + const success = await storedFileWorkingCopy.save(options); + if (success) { + return storedFileWorkingCopy; } - - // Reload async or sync based on options - else if (options?.reload) { - - // Async reload: trigger a reload but return immediately - if (options.reload.async) { - workingCopy.resolve(options); - workingCopyResolve = Promise.resolve(); - } - - // Sync reload: do not return until working copy reloaded - else { - workingCopyResolve = workingCopy.resolve(options); - } - } - - // Do not reload - else { - workingCopyResolve = Promise.resolve(); - } - } - - // File working copy does not exist - else { - didCreateWorkingCopy = true; - - const newWorkingCopy = workingCopy = this.instantiationService.createInstance(FileWorkingCopy, this.workingCopyTypeId, resource, this.labelService.getUriBasenameLabel(resource), this.modelFactory) as unknown as IFileWorkingCopy; - workingCopyResolve = workingCopy.resolve(options); - - this.registerWorkingCopy(newWorkingCopy); - } - - // Store pending resolve to avoid race conditions - this.mapResourceToPendingWorkingCopyResolve.set(resource, workingCopyResolve); - - // Make known to manager (if not already known) - this.add(resource, workingCopy); - - // Emit some events if we created the working copy - if (didCreateWorkingCopy) { - this._onDidCreate.fire(workingCopy); - - // If the working copy is dirty right from the beginning, - // make sure to emit this as an event - if (workingCopy.isDirty()) { - this._onDidChangeDirty.fire(workingCopy); - } - } - - try { - - // Wait for working copy to resolve - await workingCopyResolve; - - // Remove from pending resolves - this.mapResourceToPendingWorkingCopyResolve.delete(resource); - - // File working copy can be dirty if a backup was restored, so we make sure to - // have this event delivered if we created the working copy here - if (didCreateWorkingCopy && workingCopy.isDirty()) { - this._onDidChangeDirty.fire(workingCopy); - } - - return workingCopy; - } catch (error) { - - // Free resources of this invalid working copy - if (workingCopy) { - workingCopy.dispose(); - } - - // Remove from pending resolves - this.mapResourceToPendingWorkingCopyResolve.delete(resource); - - throw error; - } - } - - private joinPendingResolve(resource: URI): Promise | undefined { - const pendingWorkingCopyResolve = this.mapResourceToPendingWorkingCopyResolve.get(resource); - if (pendingWorkingCopyResolve) { - return pendingWorkingCopyResolve.then(undefined, error => {/* ignore any error here, it will bubble to the original requestor*/ }); } return undefined; } - private registerWorkingCopy(workingCopy: IFileWorkingCopy): void { - - // Install working copy listeners - const workingCopyListeners = new DisposableStore(); - workingCopyListeners.add(workingCopy.onDidResolve(() => this._onDidResolve.fire(workingCopy))); - workingCopyListeners.add(workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(workingCopy))); - workingCopyListeners.add(workingCopy.onDidSaveError(() => this._onDidSaveError.fire(workingCopy))); - workingCopyListeners.add(workingCopy.onDidSave(reason => this._onDidSave.fire({ workingCopy: workingCopy, reason }))); - workingCopyListeners.add(workingCopy.onDidRevert(() => this._onDidRevert.fire(workingCopy))); - - // Keep for disposal - this.mapResourceToWorkingCopyListeners.set(workingCopy.resource, workingCopyListeners); - } - - private add(resource: URI, workingCopy: IFileWorkingCopy): void { - const knownWorkingCopy = this.mapResourceToWorkingCopy.get(resource); - if (knownWorkingCopy === workingCopy) { - return; // already cached - } - - // Dispose any previously stored dispose listener for this resource - const disposeListener = this.mapResourceToDisposeListener.get(resource); - if (disposeListener) { - disposeListener.dispose(); - } - - // Store in cache but remove when working copy gets disposed - this.mapResourceToWorkingCopy.set(resource, workingCopy); - this.mapResourceToDisposeListener.set(resource, workingCopy.onWillDispose(() => this.remove(resource))); - } - - private remove(resource: URI): void { - this.mapResourceToWorkingCopy.delete(resource); - - const disposeListener = this.mapResourceToDisposeListener.get(resource); - if (disposeListener) { - dispose(disposeListener); - this.mapResourceToDisposeListener.delete(resource); - } - - const workingCopyListener = this.mapResourceToWorkingCopyListeners.get(resource); - if (workingCopyListener) { - dispose(workingCopyListener); - this.mapResourceToWorkingCopyListeners.delete(resource); - } - } - - private clear(): void { - - // Working copy caches - this.mapResourceToWorkingCopy.clear(); - this.mapResourceToPendingWorkingCopyResolve.clear(); - - // Dispose the dispose listeners - this.mapResourceToDisposeListener.forEach(listener => listener.dispose()); - this.mapResourceToDisposeListener.clear(); - - // Dispose the working copy change listeners - this.mapResourceToWorkingCopyListeners.forEach(listener => listener.dispose()); - this.mapResourceToWorkingCopyListeners.clear(); - } - - //#endregion - - //#region Save As... - - async saveAs(source: URI, target?: URI, options?: IFileWorkingCopySaveAsOptions): Promise | undefined> { - - // If not provided, ask user for target - if (!target) { - target = await this.fileDialogService.pickFileToSave(options?.suggestedTarget ?? source); - - if (!target) { - return undefined; // user canceled - } - } - - // Do it - return this.doSaveAs(source, target, options); - } - - private async doSaveAs(source: URI, target: URI, options?: IFileWorkingCopySaveAsOptions): Promise | undefined> { + private async doSaveAs(source: URI, target: URI, options?: IFileWorkingCopySaveAsOptions): Promise | undefined> { let sourceContents: VSBufferReadableStream; // If the source is an existing file working copy, we can directly @@ -616,28 +288,54 @@ export class FileWorkingCopyManager extends Dis sourceContents = (await this.fileService.readFileStream(source)).value; } - // Save the contents through working copy to benefit from save - // participants and handling a potential already existing target - return this.doSaveAsWorkingCopy(source, sourceContents, target, options); + // Resolve target + const { targetFileExists, targetStoredFileWorkingCopy } = await this.doResolveSaveTarget(source, target); + + // Confirm to overwrite if we have an untitled file working copy with associated path where + // the file actually exists on disk and we are instructed to save to that file path. + // This can happen if the file was created after the untitled file was opened. + // See https://github.com/microsoft/vscode/issues/67946 + if ( + sourceWorkingCopy instanceof UntitledFileWorkingCopy && + sourceWorkingCopy.hasAssociatedFilePath && + targetFileExists && + this.uriIdentityService.extUri.isEqual(target, toLocalResource(sourceWorkingCopy.resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme)) + ) { + const overwrite = await this.confirmOverwrite(target); + if (!overwrite) { + return undefined; + } + } + + // Take over content from source to target + await targetStoredFileWorkingCopy.model?.update(sourceContents, CancellationToken.None); + + // Save target + await targetStoredFileWorkingCopy.save({ ...options, force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ }); + + // Revert the source + await sourceWorkingCopy?.revert(); + + return targetStoredFileWorkingCopy; } - private async doSaveAsWorkingCopy(source: URI, sourceContents: VSBufferReadableStream, target: URI, options?: IFileWorkingCopySaveAsOptions): Promise> { + private async doResolveSaveTarget(source: URI, target: URI): Promise<{ targetFileExists: boolean, targetStoredFileWorkingCopy: IStoredFileWorkingCopy }> { - // Prefer an existing working copy if it is already resolved + // Prefer an existing stored file working copy if it is already resolved // for the given target resource - let targetExists = false; - let targetWorkingCopy = this.get(target); - if (targetWorkingCopy?.isResolved()) { - targetExists = true; + let targetFileExists = false; + let targetStoredFileWorkingCopy = this.stored.get(target); + if (targetStoredFileWorkingCopy?.isResolved()) { + targetFileExists = true; } // Otherwise create the target working copy empty if // it does not exist already and resolve it from there else { - targetExists = await this.fileService.exists(target); + targetFileExists = await this.fileService.exists(target); // Create target file adhoc if it does not exist yet - if (!targetExists) { + if (!targetFileExists) { await this.workingCopyFileService.create([{ resource: target }], CancellationToken.None); } @@ -645,80 +343,61 @@ export class FileWorkingCopyManager extends Dis // and we have to do an explicit check if the source URI // equals the target via URI identity. If they match and we // have had an existing working copy with the source, we - // prefer that one over resolving the target. Otherwiese we + // prefer that one over resolving the target. Otherwise we // would potentially introduce a if (this.uriIdentityService.extUri.isEqual(source, target) && this.get(source)) { - targetWorkingCopy = await this.resolve(source); + targetStoredFileWorkingCopy = await this.stored.resolve(source); } else { - targetWorkingCopy = await this.resolve(target); + targetStoredFileWorkingCopy = await this.stored.resolve(target); } } - // Take over content from source to target - await targetWorkingCopy.model?.update(sourceContents, CancellationToken.None); - - // Save target - await targetWorkingCopy.save({ ...options, force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ }); - - // Revert the source - await this.doRevert(source); - - return targetWorkingCopy; + return { targetFileExists, targetStoredFileWorkingCopy }; } - private async doRevert(resource: URI): Promise { - const workingCopy = this.get(resource); - if (!workingCopy) { - return undefined; + private async confirmOverwrite(resource: URI): Promise { + const confirm: IConfirmation = { + message: localize('confirmOverwrite', "'{0}' already exists. Do you want to replace it?", basename(resource)), + detail: localize('irreversible', "A file or folder with the name '{0}' already exists in the folder '{1}'. Replacing it will overwrite its current contents.", basename(resource), basename(dirname(resource))), + primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), + type: 'warning' + }; + + const result = await this.dialogService.confirm(confirm); + return result.confirmed; + } + + private async suggestSavePath(resource: URI): Promise { + + // 1.) Just take the resource as is if the file service can handle it + if (this.fileService.canHandleResource(resource)) { + return resource; } - return workingCopy.revert(); + // 2.) Pick the associated file path for untitled working copies if any + const workingCopy = this.get(resource); + if (workingCopy instanceof UntitledFileWorkingCopy && workingCopy.hasAssociatedFilePath) { + return toLocalResource(resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme); + } + + // 3.) Pick the working copy name if valid joined with default path + if (workingCopy && isValidBasename(workingCopy.name)) { + return joinPath(await this.fileDialogService.defaultFilePath(), workingCopy.name); + } + + // 4.) Finally fallback to the name of the resource joined with default path + return joinPath(await this.fileDialogService.defaultFilePath(), basename(resource)); } //#endregion //#region Lifecycle - canDispose(workingCopy: IFileWorkingCopy): true | Promise { - - // Quick return if working copy already disposed or not dirty and not resolving - if ( - workingCopy.isDisposed() || - (!this.mapResourceToPendingWorkingCopyResolve.has(workingCopy.resource) && !workingCopy.isDirty()) - ) { - return true; - } - - // Promise based return in all other cases - return this.doCanDispose(workingCopy); - } - - private async doCanDispose(workingCopy: IFileWorkingCopy): Promise { - - // If we have a pending working copy resolve, await it first and then try again - const pendingResolve = this.joinPendingResolve(workingCopy.resource); - if (pendingResolve) { - await pendingResolve; - - return this.canDispose(workingCopy); - } - - // Dirty working copy: we do not allow to dispose dirty working copys - // to prevent data loss cases. dirty working copys can only be disposed when - // they are either saved or reverted - if (workingCopy.isDirty()) { - await Event.toPromise(workingCopy.onDidChangeDirty); - - return this.canDispose(workingCopy); - } - - return true; - } - - override dispose(): void { - super.dispose(); - - this.clear(); + async destroy(): Promise { + await Promises.settled([ + this.stored.destroy(), + this.untitled.destroy() + ]); } //#endregion diff --git a/src/vs/workbench/services/workingCopy/common/legacyBackupRestorer.ts b/src/vs/workbench/services/workingCopy/common/legacyBackupRestorer.ts deleted file mode 100644 index 733eb9da07..0000000000 --- a/src/vs/workbench/services/workingCopy/common/legacyBackupRestorer.ts +++ /dev/null @@ -1,138 +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 { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; -import { Schemas } from 'vs/base/common/network'; -import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { IUntitledTextResourceEditorInput, IEditorInput, IEditorInputFactoryRegistry, EditorExtensions, IEditorInputWithOptions } from 'vs/workbench/common/editor'; -import { toLocalResource, isEqual } from 'vs/base/common/resources'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -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'; -import { IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopy'; - -/** - * @deprecated TODO@bpasero remove me once all backups are handled properly - */ -export class LegacyWorkingCopyBackupRestorer implements IWorkbenchContribution { - - // {{SQL CARBON EDIT}} - private static readonly SQLQUERY_REGEX = /SQLQuery\d+/; - private static readonly UNTITLED_REGEX = /Untitled-\d+/; - - private readonly editorInputFactories = Registry.as(EditorExtensions.EditorInputFactories); - - constructor( - @IEditorService private readonly editorService: IEditorService, - @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, - @ILifecycleService private readonly lifecycleService: ILifecycleService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IPathService private readonly pathService: IPathService, - @ILogService private readonly logService: ILogService - ) { - this.restoreLegacyBackups(); - } - - private restoreLegacyBackups(): void { - this.lifecycleService.when(LifecyclePhase.Restored).then(() => this.doRestoreLegacyBackups()); - } - - protected async doRestoreLegacyBackups(): Promise { - - // Resolve all backup resources that exist for this window - // that have not yet adopted the working copy editor handler - // - any working copy without `typeId` - // - not `search-edior:/` (supports migration to typeId) - const backups = (await this.workingCopyBackupService.getBackups()) - .filter(backup => backup.typeId.length === 0) - .filter(backup => backup.resource.scheme !== 'search-editor'); - - // Trigger `resolve` in each opened editor that can be found - // for the given resource and keep track of backups that are - // not opened. - const unresolvedBackups = await this.resolveOpenedBackupEditors(backups); - - // For remaining unresolved backups, explicitly open an editor - if (unresolvedBackups.length > 0) { - try { - await this.openEditors(unresolvedBackups); - } catch (error) { - this.logService.error(error); - } - - // Finally trigger `resolve` in the newly opened editors - await this.resolveOpenedBackupEditors(unresolvedBackups); - } - } - - private async resolveOpenedBackupEditors(backups: readonly IWorkingCopyIdentifier[]): Promise { - const unresolvedBackups: IWorkingCopyIdentifier[] = []; - - await Promises.settled(backups.map(async backup => { - const openedEditor = this.findOpenedEditor(backup); - if (openedEditor) { - try { - await openedEditor.resolve(); - } catch (error) { - unresolvedBackups.push(backup); // ignore error and remember as unresolved - } - } else { - unresolvedBackups.push(backup); - } - })); - - return unresolvedBackups; - } - - private findOpenedEditor(backup: IWorkingCopyIdentifier): IEditorInput | undefined { - for (const editor of this.editorService.editors) { - const customFactory = this.editorInputFactories.getCustomEditorInputFactory(backup.resource.scheme); - if (customFactory?.canResolveBackup(editor, backup.resource) || isEqual(editor.resource, backup.resource)) { - return editor; - } - } - - return undefined; - } - - private async openEditors(backups: IWorkingCopyIdentifier[]): Promise { - const hasOpenedEditors = this.editorService.visibleEditors.length > 0; - const editors = await Promises.settled(backups.map((backup, index) => this.resolveEditor(backup, index, hasOpenedEditors))); - - await this.editorService.openEditors(editors); - } - - private async resolveEditor(backup: IWorkingCopyIdentifier, index: number, hasOpenedEditors: boolean): Promise { - - // Set editor as `inactive` if we have other editors - const options = { pinned: true, preserveFocus: true, inactive: index > 0 || hasOpenedEditors }; - - // This is a (weak) strategy to find out if the untitled input had - // an associated file path or not by just looking at the path. and - // if so, we must ensure to restore the local resource it had. - if (backup.resource.scheme === Schemas.untitled && !LegacyWorkingCopyBackupRestorer.UNTITLED_REGEX.test(backup.resource.path) && LegacyWorkingCopyBackupRestorer.SQLQUERY_REGEX.test(backup.resource.path)) { - return { resource: toLocalResource(backup.resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme), options, forceUntitled: true }; - } - - // Handle custom editors by asking the custom editor input factory - // to create the input. - const customFactory = this.editorInputFactories.getCustomEditorInputFactory(backup.resource.scheme); - if (customFactory) { - const editor = await customFactory.createCustomEditorInput(backup.resource, this.instantiationService); - - return { editor, options }; - } - - // Finally return with a simple resource based input - return { resource: backup.resource, options }; - } -} diff --git a/src/vs/workbench/services/workingCopy/common/resourceWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/resourceWorkingCopy.ts new file mode 100644 index 0000000000..61ba96f493 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/resourceWorkingCopy.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 { timeout } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Event, Emitter } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/files/common/files'; +import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; +import { IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; + +/** + * A resource based `IWorkingCopy` is backed by a `URI` from a + * known file system provider. + */ +export interface IResourceWorkingCopy extends IWorkingCopy, IDisposable { + + /** + * An event for when the orphaned state of the resource working copy changes. + */ + readonly onDidChangeOrphaned: Event; + + /** + * Whether the resource working copy is orphaned or not. + */ + isOrphaned(): boolean; + + /** + * An event for when the file working copy has been disposed. + */ + readonly onWillDispose: Event; + + /** + * Whether the file working copy has been disposed or not. + */ + isDisposed(): boolean; +} + +export abstract class ResourceWorkingCopy extends Disposable implements IResourceWorkingCopy { + + constructor( + readonly resource: URI, + @IFileService protected readonly fileService: IFileService + ) { + super(); + + this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e))); + } + + //#region Orphaned Tracking + + private readonly _onDidChangeOrphaned = this._register(new Emitter()); + readonly onDidChangeOrphaned = this._onDidChangeOrphaned.event; + + private orphaned = false; + + isOrphaned(): boolean { + return this.orphaned; + } + + private async onDidFilesChange(e: FileChangesEvent): Promise { + let fileEventImpactsUs = false; + let newInOrphanModeGuess: boolean | undefined; + + // If we are currently orphaned, we check if the file was added back + if (this.orphaned) { + const fileWorkingCopyResourceAdded = e.contains(this.resource, FileChangeType.ADDED); + if (fileWorkingCopyResourceAdded) { + newInOrphanModeGuess = false; + fileEventImpactsUs = true; + } + } + + // Otherwise we check if the file was deleted + else { + const fileWorkingCopyResourceDeleted = e.contains(this.resource, FileChangeType.DELETED); + if (fileWorkingCopyResourceDeleted) { + newInOrphanModeGuess = true; + fileEventImpactsUs = true; + } + } + + if (fileEventImpactsUs && this.orphaned !== newInOrphanModeGuess) { + let newInOrphanModeValidated: boolean = false; + if (newInOrphanModeGuess) { + + // We have received reports of users seeing delete events even though the file still + // exists (network shares issue: https://github.com/microsoft/vscode/issues/13665). + // Since we do not want to mark the working copy as orphaned, we have to check if the + // file is really gone and not just a faulty file event. + await timeout(100); + + if (this.isDisposed()) { + newInOrphanModeValidated = true; + } else { + const exists = await this.fileService.exists(this.resource); + newInOrphanModeValidated = !exists; + } + } + + if (this.orphaned !== newInOrphanModeValidated && !this.isDisposed()) { + this.setOrphaned(newInOrphanModeValidated); + } + } + } + + protected setOrphaned(orphaned: boolean): void { + if (this.orphaned !== orphaned) { + this.orphaned = orphaned; + + this._onDidChangeOrphaned.fire(); + } + } + + //#endregion + + + //#region Dispose + + private readonly _onWillDispose = this._register(new Emitter()); + readonly onWillDispose = this._onWillDispose.event; + + private disposed = false; + + isDisposed(): boolean { + return this.disposed; + } + + override dispose(): void { + + // State + this.disposed = true; + this.orphaned = false; + + // Event + this._onWillDispose.fire(); + + super.dispose(); + } + + //#endregion + + + //#region Abstract + + abstract name: string; + abstract capabilities: WorkingCopyCapabilities; + abstract onDidChangeDirty: Event; + abstract onDidChangeContent: Event; + abstract isDirty(): boolean; + abstract backup(token: CancellationToken): Promise; + abstract save(options?: ISaveOptions): Promise; + abstract revert(options?: IRevertOptions): Promise; + abstract typeId: string; + + //#endregion +} diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts new file mode 100644 index 0000000000..6be6d78bdf --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts @@ -0,0 +1,1212 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { URI } from 'vs/base/common/uri'; +import { Event, Emitter } from 'vs/base/common/event'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { ETAG_DISABLED, FileOperationError, FileOperationResult, FileSystemProviderCapabilities, IFileService, IFileStatWithMetadata, IFileStreamContent, IWriteFileOptions, NotModifiedSinceFileOperationError } from 'vs/platform/files/common/files'; +import { ISaveOptions, IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyBackup, IWorkingCopyBackupMeta, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { raceCancellation, TaskSequentializer, timeout } from 'vs/base/common/async'; +import { ILogService } from 'vs/platform/log/common/log'; +import { assertIsDefined } from 'vs/base/common/types'; +import { ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { VSBufferReadableStream } from 'vs/base/common/buffer'; +import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IWorkingCopyBackupService, IResolvedWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { hash } from 'vs/base/common/hash'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { IAction, toAction } from 'vs/base/common/actions'; +import { isWindows } from 'vs/base/common/platform'; +import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IElevatedFileService } from 'vs/workbench/services/files/common/elevatedFileService'; +import { IResourceWorkingCopy, ResourceWorkingCopy } from 'vs/workbench/services/workingCopy/common/resourceWorkingCopy'; +import { IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; + +/** + * Stored file specific working copy model factory. + */ +export interface IStoredFileWorkingCopyModelFactory extends IFileWorkingCopyModelFactory { } + +/** + * The underlying model of a stored file working copy provides some + * methods for the stored file working copy to function. The model is + * typically only available after the working copy has been + * resolved via it's `resolve()` method. + */ +export interface IStoredFileWorkingCopyModel extends IFileWorkingCopyModel { + + readonly onDidChangeContent: Event; + + /** + * A version ID of the model. If a `onDidChangeContent` is fired + * from the model and the last known saved `versionId` matches + * with the `model.versionId`, the stored file working copy will + * discard any dirty state. + * + * A use case is the following: + * - a stored file working copy gets edited and thus dirty + * - the user triggers undo to revert the changes + * - at this point the `versionId` should match the one we had saved + * + * This requires the model to be aware of undo/redo operations. + */ + readonly versionId: unknown; + + /** + * Close the current undo-redo element. This offers a way + * to create an undo/redo stop point. + * + * This method may for example be called right before the + * save is triggered so that the user can always undo back + * to the state before saving. + */ + pushStackElement(): void; +} + +export interface IStoredFileWorkingCopyModelContentChangedEvent { + + /** + * Flag that indicates that this event was generated while undoing. + */ + readonly isUndoing: boolean; + + /** + * Flag that indicates that this event was generated while redoing. + */ + readonly isRedoing: boolean; +} + +/** + * A stored file based `IWorkingCopy` is backed by a `URI` from a + * known file system provider. Given this assumption, a lot + * of functionality can be built on top, such as saving in + * a secure way to prevent data loss. + */ +export interface IStoredFileWorkingCopy extends IResourceWorkingCopy, IFileWorkingCopy { + + /** + * An event for when a stored file working copy was resolved. + */ + readonly onDidResolve: Event; + + /** + * An event for when a stored file working copy was saved successfully. + */ + readonly onDidSave: Event; + + /** + * An event indicating that a stored file working copy save operation failed. + */ + readonly onDidSaveError: Event; + + /** + * An event for when the readonly state of the stored file working copy changes. + */ + readonly onDidChangeReadonly: Event; + + /** + * Resolves a stored file working copy. + */ + resolve(options?: IStoredFileWorkingCopyResolveOptions): Promise; + + /** + * Explicitly sets the working copy to be dirty. + */ + markDirty(): void; + + /** + * Whether the stored file working copy is in the provided `state` + * or not. + * + * @param state the `FileWorkingCopyState` to check on. + */ + hasState(state: StoredFileWorkingCopyState): boolean; + + /** + * Allows to join a state change away from the provided `state`. + * + * @param state currently only `FileWorkingCopyState.PENDING_SAVE` + * can be awaited on to resolve. + */ + joinState(state: StoredFileWorkingCopyState.PENDING_SAVE): Promise; + + /** + * Whether we have a resolved model or not. + */ + isResolved(): this is IResolvedStoredFileWorkingCopy; + + /** + * Whether the stored file working copy is readonly or not. + */ + isReadonly(): boolean; +} + +export interface IResolvedStoredFileWorkingCopy extends IStoredFileWorkingCopy { + + /** + * A resolved stored file working copy has a resolved model. + */ + readonly model: M; +} + +/** + * States the stored file working copy can be in. + */ +export const enum StoredFileWorkingCopyState { + + /** + * A stored file working copy is saved. + */ + SAVED, + + /** + * A stored file working copy is dirty. + */ + DIRTY, + + /** + * A stored file working copy is currently being saved but + * this operation has not completed yet. + */ + PENDING_SAVE, + + /** + * A stored file working copy is in conflict mode when changes + * cannot be saved because the underlying file has changed. + * Stored file working copies in conflict mode are always dirty. + */ + CONFLICT, + + /** + * A stored file working copy is in orphan state when the underlying + * file has been deleted. + */ + ORPHAN, + + /** + * Any error that happens during a save that is not causing + * the `StoredFileWorkingCopyState.CONFLICT` state. + * Stored file working copies in error mode are always dirty. + */ + ERROR +} + +export interface IStoredFileWorkingCopySaveOptions extends ISaveOptions { + + /** + * Save the stored file working copy with an attempt to unlock it. + */ + writeUnlock?: boolean; + + /** + * Save the stored file working copy with elevated privileges. + * + * Note: This may not be supported in all environments. + */ + writeElevated?: boolean; + + /** + * Allows to write to a stored file working copy even if it has been + * modified on disk. This should only be triggered from an + * explicit user action. + */ + ignoreModifiedSince?: boolean; + + /** + * If set, will bubble up the stored file working copy save error to + * the caller instead of handling it. + */ + ignoreErrorHandler?: boolean; +} + +export interface IStoredFileWorkingCopyResolveOptions { + + /** + * The contents to use for the stored file working copy if known. If not + * provided, the contents will be retrieved from the underlying + * resource or backup if present. + * + * If contents are provided, the stored file working copy will be marked + * as dirty right from the beginning. + */ + contents?: VSBufferReadableStream; + + /** + * Go to disk bypassing any cache of the stored file working copy if any. + */ + forceReadFromFile?: boolean; +} + +/** + * Metadata associated with a stored file working copy backup. + */ +interface IStoredFileWorkingCopyBackupMetaData extends IWorkingCopyBackupMeta { + mtime: number; + ctime: number; + size: number; + etag: string; + orphaned: boolean; +} + +export class StoredFileWorkingCopy extends ResourceWorkingCopy implements IStoredFileWorkingCopy { + + readonly capabilities: WorkingCopyCapabilities = WorkingCopyCapabilities.None; + + private _model: M | undefined = undefined; + get model(): M | undefined { return this._model; } + + //#region events + + private readonly _onDidChangeContent = this._register(new Emitter()); + readonly onDidChangeContent = this._onDidChangeContent.event; + + private readonly _onDidResolve = this._register(new Emitter()); + readonly onDidResolve = this._onDidResolve.event; + + private readonly _onDidChangeDirty = this._register(new Emitter()); + readonly onDidChangeDirty = this._onDidChangeDirty.event; + + private readonly _onDidSaveError = this._register(new Emitter()); + readonly onDidSaveError = this._onDidSaveError.event; + + private readonly _onDidSave = this._register(new Emitter()); + readonly onDidSave = this._onDidSave.event; + + private readonly _onDidRevert = this._register(new Emitter()); + readonly onDidRevert = this._onDidRevert.event; + + private readonly _onDidChangeReadonly = this._register(new Emitter()); + readonly onDidChangeReadonly = this._onDidChangeReadonly.event; + + //#endregion + + constructor( + readonly typeId: string, + resource: URI, + readonly name: string, + private readonly modelFactory: IStoredFileWorkingCopyModelFactory, + @IFileService fileService: IFileService, + @ILogService private readonly logService: ILogService, + @ITextFileService private readonly textFileService: ITextFileService, + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, + @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, + @IWorkingCopyService workingCopyService: IWorkingCopyService, + @INotificationService private readonly notificationService: INotificationService, + @IWorkingCopyEditorService private readonly workingCopyEditorService: IWorkingCopyEditorService, + @IEditorService private readonly editorService: IEditorService, + @IElevatedFileService private readonly elevatedFileService: IElevatedFileService + ) { + super(resource, fileService); + + // Make known to working copy service + this._register(workingCopyService.registerWorkingCopy(this)); + } + + //#region Dirty + + private dirty = false; + private savedVersionId: unknown; + + isDirty(): this is IResolvedStoredFileWorkingCopy { + return this.dirty; + } + + markDirty(): void { + this.setDirty(true); + } + + private setDirty(dirty: boolean): void { + if (!this.isResolved()) { + return; // only resolved working copies can be marked dirty + } + + // Track dirty state and version id + const wasDirty = this.dirty; + this.doSetDirty(dirty); + + // Emit as Event if dirty changed + if (dirty !== wasDirty) { + this._onDidChangeDirty.fire(); + } + } + + private doSetDirty(dirty: boolean): () => void { + const wasDirty = this.dirty; + const wasInConflictMode = this.inConflictMode; + const wasInErrorMode = this.inErrorMode; + const oldSavedVersionId = this.savedVersionId; + + if (!dirty) { + this.dirty = false; + this.inConflictMode = false; + this.inErrorMode = false; + + // we remember the models alternate version id to remember when the version + // of the model matches with the saved version on disk. we need to keep this + // in order to find out if the model changed back to a saved version (e.g. + // when undoing long enough to reach to a version that is saved and then to + // clear the dirty flag) + if (this.isResolved()) { + this.savedVersionId = this.model.versionId; + } + } else { + this.dirty = true; + } + + // Return function to revert this call + return () => { + this.dirty = wasDirty; + this.inConflictMode = wasInConflictMode; + this.inErrorMode = wasInErrorMode; + this.savedVersionId = oldSavedVersionId; + }; + } + + //#endregion + + //#region Resolve + + private lastResolvedFileStat: IFileStatWithMetadata | undefined; + + isResolved(): this is IResolvedStoredFileWorkingCopy { + return !!this.model; + } + + async resolve(options?: IStoredFileWorkingCopyResolveOptions): Promise { + this.trace('[stored file working copy] resolve() - enter'); + + // Return early if we are disposed + if (this.isDisposed()) { + this.trace('[stored file working copy] resolve() - exit - without resolving because file working copy is disposed'); + + return; + } + + // Unless there are explicit contents provided, it is important that we do not + // resolve a working copy that is dirty or is in the process of saving to prevent + // data loss. + if (!options?.contents && (this.dirty || this.saveSequentializer.hasPending())) { + this.trace('[stored file working copy] resolve() - exit - without resolving because file working copy is dirty or being saved'); + + return; + } + + return this.doResolve(options); + } + + private async doResolve(options?: IStoredFileWorkingCopyResolveOptions): Promise { + + // First check if we have contents to use for the working copy + if (options?.contents) { + return this.resolveFromBuffer(options.contents); + } + + // Second, check if we have a backup to resolve from (only for new working copies) + const isNew = !this.isResolved(); + if (isNew) { + const resolvedFromBackup = await this.resolveFromBackup(); + if (resolvedFromBackup) { + return; + } + } + + // Finally, resolve from file resource + return this.resolveFromFile(options); + } + + private async resolveFromBuffer(buffer: VSBufferReadableStream): Promise { + this.trace('[stored file working copy] resolveFromBuffer()'); + + // Try to resolve metdata from disk + let mtime: number; + let ctime: number; + let size: number; + let etag: string; + try { + const metadata = await this.fileService.resolve(this.resource, { resolveMetadata: true }); + mtime = metadata.mtime; + ctime = metadata.ctime; + size = metadata.size; + etag = metadata.etag; + + // Clear orphaned state when resolving was successful + this.setOrphaned(false); + } catch (error) { + + // Put some fallback values in error case + mtime = Date.now(); + ctime = Date.now(); + size = 0; + etag = ETAG_DISABLED; + + // Apply orphaned state based on error code + this.setOrphaned(error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND); + } + + // Resolve with buffer + return this.resolveFromContent({ + resource: this.resource, + name: this.name, + mtime, + ctime, + size, + etag, + value: buffer, + readonly: false + }, true /* dirty (resolved from buffer) */); + } + + private async resolveFromBackup(): Promise { + + // Resolve backup if any + const backup = await this.workingCopyBackupService.resolve(this); + + // Abort if someone else managed to resolve the working copy by now + let isNew = !this.isResolved(); + if (!isNew) { + this.trace('[stored file working copy] resolveFromBackup() - exit - withoutresolving because previously new file working copy got created meanwhile'); + + return true; // imply that resolving has happened in another operation + } + + // Try to resolve from backup if we have any + if (backup) { + await this.doResolveFromBackup(backup); + + return true; + } + + // Otherwise signal back that resolving did not happen + return false; + } + + private async doResolveFromBackup(backup: IResolvedWorkingCopyBackup): Promise { + this.trace('[stored file working copy] doResolveFromBackup()'); + + // Resolve with backup + await this.resolveFromContent({ + resource: this.resource, + name: this.name, + mtime: backup.meta ? backup.meta.mtime : Date.now(), + ctime: backup.meta ? backup.meta.ctime : Date.now(), + size: backup.meta ? backup.meta.size : 0, + etag: backup.meta ? backup.meta.etag : ETAG_DISABLED, // etag disabled if unknown! + value: backup.value, + readonly: false + }, true /* dirty (resolved from backup) */); + + // Restore orphaned flag based on state + if (backup.meta && backup.meta.orphaned) { + this.setOrphaned(true); + } + } + + private async resolveFromFile(options?: IStoredFileWorkingCopyResolveOptions): Promise { + this.trace('[stored file working copy] resolveFromFile()'); + + const forceReadFromFile = options?.forceReadFromFile; + + // Decide on etag + let etag: string | undefined; + if (forceReadFromFile) { + etag = ETAG_DISABLED; // disable ETag if we enforce to read from disk + } else if (this.lastResolvedFileStat) { + etag = this.lastResolvedFileStat.etag; // otherwise respect etag to support caching + } + + // Remember current version before doing any long running operation + // to ensure we are not changing a working copy that was changed + // meanwhile + const currentVersionId = this.versionId; + + // Resolve Content + try { + const content = await this.fileService.readFileStream(this.resource, { etag }); + + // Clear orphaned state when resolving was successful + this.setOrphaned(false); + + // Return early if the working copy content has changed + // meanwhile to prevent loosing any changes + if (currentVersionId !== this.versionId) { + this.trace('[stored file working copy] resolveFromFile() - exit - without resolving because file working copy content changed'); + + return; + } + + await this.resolveFromContent(content, false /* not dirty (resolved from file) */); + } catch (error) { + const result = error.fileOperationResult; + + // Apply orphaned state based on error code + this.setOrphaned(result === FileOperationResult.FILE_NOT_FOUND); + + // NotModified status is expected and can be handled gracefully + // if we are resolved. We still want to update our last resolved + // stat to e.g. detect changes to the file's readonly state + if (this.isResolved() && result === FileOperationResult.FILE_NOT_MODIFIED_SINCE) { + if (error instanceof NotModifiedSinceFileOperationError) { + this.updateLastResolvedFileStat(error.stat); + } + + return; + } + + // Unless we are forced to read from the file, ignore when a working copy has + // been resolved once and the file was deleted meanwhile. Since we already have + // the working copy resolved, we can return to this state and update the orphaned + // flag to indicate that this working copy has no version on disk anymore. + if (this.isResolved() && result === FileOperationResult.FILE_NOT_FOUND && !forceReadFromFile) { + return; + } + + // Otherwise bubble up the error + throw error; + } + } + + private async resolveFromContent(content: IFileStreamContent, dirty: boolean): Promise { + this.trace('[stored file working copy] resolveFromContent() - enter'); + + // Return early if we are disposed + if (this.isDisposed()) { + this.trace('[stored file working copy] resolveFromContent() - exit - because working copy is disposed'); + + return; + } + + // Update our resolved disk stat + this.updateLastResolvedFileStat({ + resource: this.resource, + name: content.name, + mtime: content.mtime, + ctime: content.ctime, + size: content.size, + etag: content.etag, + readonly: content.readonly, + isFile: true, + isDirectory: false, + isSymbolicLink: false + }); + + // Update existing model if we had been resolved + if (this.isResolved()) { + await this.doUpdateModel(content.value); + } + + // Create new model otherwise + else { + await (this as StoredFileWorkingCopy).doCreateModel(content.value); // {{SQL CARBON EDIT}} + } + + // Update working copy dirty flag. This is very important to call + // in both cases of dirty or not because it conditionally updates + // the `savedVersionId` to determine the version when to consider + // the working copy as saved again (e.g. when undoing back to the + // saved state) + this.setDirty(!!dirty); + + // Emit as event + this._onDidResolve.fire(); + } + + private async doCreateModel(contents: VSBufferReadableStream): Promise { + this.trace('[stored file working copy] doCreateModel()'); + + // Create model and dispose it when we get disposed + this._model = this._register(await this.modelFactory.createModel(this.resource, contents, CancellationToken.None)); + + // Model listeners + this.installModelListeners(this._model); + } + + private ignoreDirtyOnModelContentChange = false; + + private async doUpdateModel(contents: VSBufferReadableStream): Promise { + this.trace('[stored file working copy] doUpdateModel()'); + + // Update model value in a block that ignores content change events for dirty tracking + this.ignoreDirtyOnModelContentChange = true; + try { + await this.model?.update(contents, CancellationToken.None); + } finally { + this.ignoreDirtyOnModelContentChange = false; + } + } + + private installModelListeners(model: M): void { + + // See https://github.com/microsoft/vscode/issues/30189 + // This code has been extracted to a different method because it caused a memory leak + // where `value` was captured in the content change listener closure scope. + + // Content Change + this._register(model.onDidChangeContent(e => this.onModelContentChanged(model, e.isUndoing || e.isRedoing))); + + // Lifecycle + this._register(model.onWillDispose(() => this.dispose())); + } + + private onModelContentChanged(model: M, isUndoingOrRedoing: boolean): void { + this.trace(`[stored file working copy] onModelContentChanged() - enter`); + + // In any case increment the version id because it tracks the textual content state of the model at all times + this.versionId++; + this.trace(`[stored file working copy] onModelContentChanged() - new versionId ${this.versionId}`); + + // Remember when the user changed the model through a undo/redo operation. + // We need this information to throttle save participants to fix + // https://github.com/microsoft/vscode/issues/102542 + if (isUndoingOrRedoing) { + this.lastContentChangeFromUndoRedo = Date.now(); + } + + // We mark check for a dirty-state change upon model content change, unless: + // - explicitly instructed to ignore it (e.g. from model.resolve()) + // - the model is readonly (in that case we never assume the change was done by the user) + if (!this.ignoreDirtyOnModelContentChange && !this.isReadonly()) { + + // The contents changed as a matter of Undo and the version reached matches the saved one + // In this case we clear the dirty flag and emit a SAVED event to indicate this state. + if (model.versionId === this.savedVersionId) { + this.trace('[stored file working copy] onModelContentChanged() - model content changed back to last saved version'); + + // Clear flags + const wasDirty = this.dirty; + this.setDirty(false); + + // Emit revert event if we were dirty + if (wasDirty) { + this._onDidRevert.fire(); + } + } + + // Otherwise the content has changed and we signal this as becoming dirty + else { + this.trace('[stored file working copy] onModelContentChanged() - model content changed and marked as dirty'); + + // Mark as dirty + this.setDirty(true); + } + } + + // Emit as event + this._onDidChangeContent.fire(); + } + + //#endregion + + //#region Backup + + async backup(token: CancellationToken): Promise { + + // Fill in metadata if we are resolved + let meta: IStoredFileWorkingCopyBackupMetaData | undefined = undefined; + if (this.lastResolvedFileStat) { + meta = { + mtime: this.lastResolvedFileStat.mtime, + ctime: this.lastResolvedFileStat.ctime, + size: this.lastResolvedFileStat.size, + etag: this.lastResolvedFileStat.etag, + orphaned: this.isOrphaned() + }; + } + + // Fill in content if we are resolved + let content: VSBufferReadableStream | undefined = undefined; + if (this.isResolved()) { + content = await raceCancellation(this.model.snapshot(token), token); + } + + return { meta, content }; + } + + //#endregion + + //#region Save + + private versionId = 0; + + private static readonly UNDO_REDO_SAVE_PARTICIPANTS_AUTO_SAVE_THROTTLE_THRESHOLD = 500; + private lastContentChangeFromUndoRedo: number | undefined = undefined; + + private readonly saveSequentializer = new TaskSequentializer(); + + async save(options: IStoredFileWorkingCopySaveOptions = Object.create(null)): Promise { + if (!this.isResolved()) { + return false; + } + + if (this.isReadonly()) { + this.trace('[stored file working copy] save() - ignoring request for readonly resource'); + + return false; // if working copy is readonly we do not attempt to save at all + } + + if ( + (this.hasState(StoredFileWorkingCopyState.CONFLICT) || this.hasState(StoredFileWorkingCopyState.ERROR)) && + (options.reason === SaveReason.AUTO || options.reason === SaveReason.FOCUS_CHANGE || options.reason === SaveReason.WINDOW_CHANGE) + ) { + this.trace('[stored file working copy] save() - ignoring auto save request for file working copy that is in conflict or error'); + + return false; // if working copy is in save conflict or error, do not save unless save reason is explicit + } + + // Actually do save + this.trace('[stored file working copy] save() - enter'); + await this.doSave(options); + this.trace('[stored file working copy] save() - exit'); + + return true; + } + + private async doSave(options: IStoredFileWorkingCopySaveOptions): Promise { + if (typeof options.reason !== 'number') { + options.reason = SaveReason.EXPLICIT; + } + + let versionId = this.versionId; + this.trace(`[stored file working copy] doSave(${versionId}) - enter with versionId ${versionId}`); + + // Lookup any running pending save for this versionId and return it if found + // + // Scenario: user invoked the save action multiple times quickly for the same contents + // while the save was not yet finished to disk + // + if (this.saveSequentializer.hasPending(versionId)) { + this.trace(`[stored file working copy] doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`); + + return this.saveSequentializer.pending; + } + + // Return early if not dirty (unless forced) + // + // Scenario: user invoked save action even though the working copy is not dirty + if (!options.force && !this.dirty) { + this.trace(`[stored file working copy] doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`); + + return; + } + + // Return if currently saving by storing this save request as the next save that should happen. + // Never ever must 2 saves execute at the same time because this can lead to dirty writes and race conditions. + // + // Scenario A: auto save was triggered and is currently busy saving to disk. this takes long enough that another auto save + // kicks in. + // Scenario B: save is very slow (e.g. network share) and the user manages to change the working copy and trigger another save + // while the first save has not returned yet. + // + if ((this.saveSequentializer as TaskSequentializer).hasPending()) { // {{SQL CARBON EDIT}} + this.trace(`[stored file working copy] doSave(${versionId}) - exit - because busy saving`); + + // Indicate to the save sequentializer that we want to + // cancel the pending operation so that ours can run + // before the pending one finishes. + // Currently this will try to cancel pending save + // participants and pending snapshots from the + // save operation, but not the actual save which does + // not support cancellation yet. + (this.saveSequentializer as TaskSequentializer).cancelPending();// {{SQL CARBON EDIT}} + + // Register this as the next upcoming save and return + return (this.saveSequentializer as TaskSequentializer).setNext(() => this.doSave(options));// {{SQL CARBON EDIT}} + } + + // Push all edit operations to the undo stack so that the user has a chance to + // Ctrl+Z back to the saved version. + if (this.isResolved()) { + this.model.pushStackElement(); + } + + const saveCancellation = new CancellationTokenSource(); + + return (this.saveSequentializer as TaskSequentializer).setPending(versionId, (async () => { // {{SQL CARBON EDIT}} + + // A save participant can still change the working copy now + // and since we are so close to saving we do not want to trigger + // another auto save or similar, so we block this + // In addition we update our version right after in case it changed + // because of a working copy change + // Save participants can also be skipped through API. + if (this.isResolved() && !options.skipSaveParticipants && this.isTextFileModel(this.model)) { + try { + + // Measure the time it took from the last undo/redo operation to this save. If this + // time is below `UNDO_REDO_SAVE_PARTICIPANTS_THROTTLE_THRESHOLD`, we make sure to + // delay the save participant for the remaining time if the reason is auto save. + // + // This fixes the following issue: + // - the user has configured auto save with delay of 100ms or shorter + // - the user has a save participant enabled that modifies the file on each save + // - the user types into the file and the file gets saved + // - the user triggers undo operation + // - this will undo the save participant change but trigger the save participant right after + // - the user has no chance to undo over the save participant + // + // Reported as: https://github.com/microsoft/vscode/issues/102542 + if (options.reason === SaveReason.AUTO && typeof this.lastContentChangeFromUndoRedo === 'number') { + const timeFromUndoRedoToSave = Date.now() - this.lastContentChangeFromUndoRedo; + if (timeFromUndoRedoToSave < StoredFileWorkingCopy.UNDO_REDO_SAVE_PARTICIPANTS_AUTO_SAVE_THROTTLE_THRESHOLD) { + await timeout(StoredFileWorkingCopy.UNDO_REDO_SAVE_PARTICIPANTS_AUTO_SAVE_THROTTLE_THRESHOLD - timeFromUndoRedoToSave); + } + } + + // Run save participants unless save was cancelled meanwhile + if (!saveCancellation.token.isCancellationRequested) { + await this.textFileService.files.runSaveParticipants(this.model, { reason: options.reason ?? SaveReason.EXPLICIT }, saveCancellation.token); + } + } catch (error) { + this.logService.error(`[stored file working copy] runSaveParticipants(${versionId}) - resulted in an error: ${error.toString()}`, this.resource.toString(true), this.typeId); + } + } + + // It is possible that a subsequent save is cancelling this + // running save. As such we return early when we detect that. + if (saveCancellation.token.isCancellationRequested) { + return; + } + + // We have to protect against being disposed at this point. It could be that the save() operation + // was triggerd followed by a dispose() operation right after without waiting. Typically we cannot + // be disposed if we are dirty, but if we are not dirty, save() and dispose() can still be triggered + // one after the other without waiting for the save() to complete. If we are disposed(), we risk + // saving contents to disk that are stale (see https://github.com/microsoft/vscode/issues/50942). + // To fix this issue, we will not store the contents to disk when we got disposed. + if (this.isDisposed()) { + return; + } + + // We require a resolved working copy from this point on, since we are about to write data to disk. + if (!this.isResolved()) { + return; + } + + // update versionId with its new value (if pre-save changes happened) + versionId = this.versionId; + + // Clear error flag since we are trying to save again + this.inErrorMode = false; + + // Save to Disk. We mark the save operation as currently pending with + // the latest versionId because it might have changed from a save + // participant triggering + this.trace(`[stored file working copy] doSave(${versionId}) - before write()`); + const lastResolvedFileStat = assertIsDefined(this.lastResolvedFileStat); + const resolvedFileWorkingCopy = this; + return this.saveSequentializer.setPending(versionId, (async () => { + try { + + // Snapshot working copy model contents + const snapshot = await raceCancellation(resolvedFileWorkingCopy.model.snapshot(saveCancellation.token), saveCancellation.token); + + // It is possible that a subsequent save is cancelling this + // running save. As such we return early when we detect that + // However, we do not pass the token into the file service + // because that is an atomic operation currently without + // cancellation support, so we dispose the cancellation if + // it was not cancelled yet. + if (saveCancellation.token.isCancellationRequested) { + return; + } else { + saveCancellation.dispose(); + } + + const writeFileOptions: IWriteFileOptions = { + mtime: lastResolvedFileStat.mtime, + etag: (options.ignoreModifiedSince || !this.filesConfigurationService.preventSaveConflicts(lastResolvedFileStat.resource)) ? ETAG_DISABLED : lastResolvedFileStat.etag, + unlock: options.writeUnlock + }; + + // Write them to disk + let stat: IFileStatWithMetadata; + if (options?.writeElevated && this.elevatedFileService.isSupported(lastResolvedFileStat.resource)) { + stat = await this.elevatedFileService.writeFileElevated(lastResolvedFileStat.resource, assertIsDefined(snapshot), writeFileOptions); + } else { + stat = await this.fileService.writeFile(lastResolvedFileStat.resource, assertIsDefined(snapshot), writeFileOptions); + } + + this.handleSaveSuccess(stat, versionId, options); + } catch (error) { + this.handleSaveError(error, versionId, options); + } + })(), () => saveCancellation.cancel()); + })(), () => saveCancellation.cancel()); + } + + private handleSaveSuccess(stat: IFileStatWithMetadata, versionId: number, options: IStoredFileWorkingCopySaveOptions): void { + + // Updated resolved stat with updated stat + this.updateLastResolvedFileStat(stat); + + // Update dirty state unless working copy has changed meanwhile + if (versionId === this.versionId) { + this.trace(`[stored file working copy] handleSaveSuccess(${versionId}) - setting dirty to false because versionId did not change`); + this.setDirty(false); + } else { + this.trace(`[stored file working copy] handleSaveSuccess(${versionId}) - not setting dirty to false because versionId did change meanwhile`); + } + + // Update orphan state given save was successful + this.setOrphaned(false); + + // Emit Save Event + this._onDidSave.fire(options.reason ?? SaveReason.EXPLICIT); + } + + private handleSaveError(error: Error, versionId: number, options: IStoredFileWorkingCopySaveOptions): void { + this.logService.error(`[stored file working copy] handleSaveError(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource.toString(true), this.typeId); + + // Return early if the save() call was made asking to + // handle the save error itself. + if (options.ignoreErrorHandler) { + throw error; + } + + // In any case of an error, we mark the working copy as dirty to prevent data loss + // It could be possible that the write corrupted the file on disk (e.g. when + // an error happened after truncating the file) and as such we want to preserve + // the working copy contents to prevent data loss. + this.setDirty(true); + + // Flag as error state + this.inErrorMode = true; + + // Look out for a save conflict + if ((error as FileOperationError).fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) { + this.inConflictMode = true; + } + + // Delegate to save error handler + if (this.isTextFileModel(this.model)) { + this.textFileService.files.saveErrorHandler.onSaveError(error, this.model); + } else { + this.doHandleSaveError(error); + } + + // Emit as event + this._onDidSaveError.fire(); + } + + private doHandleSaveError(error: Error): void { + const fileOperationError = error as FileOperationError; + const primaryActions: IAction[] = []; + + let message: string; + + // Dirty write prevention + if (fileOperationError.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) { + message = localize('staleSaveError', "Failed to save '{0}': The content of the file is newer. Do you want to overwrite the file with your changes?", this.name); + + primaryActions.push(toAction({ id: 'fileWorkingCopy.overwrite', label: localize('overwrite', "Overwrite"), run: () => this.save({ ignoreModifiedSince: true }) })); + primaryActions.push(toAction({ id: 'fileWorkingCopy.revert', label: localize('discard', "Discard"), run: () => this.revert() })); + } + + // Any other save error + else { + const isWriteLocked = fileOperationError.fileOperationResult === FileOperationResult.FILE_WRITE_LOCKED; + const triedToUnlock = isWriteLocked && fileOperationError.options?.unlock; + const isPermissionDenied = fileOperationError.fileOperationResult === FileOperationResult.FILE_PERMISSION_DENIED; + const canSaveElevated = this.elevatedFileService.isSupported(this.resource); + + // Save Elevated + if (canSaveElevated && (isPermissionDenied || triedToUnlock)) { + primaryActions.push(toAction({ + id: 'fileWorkingCopy.saveElevated', + label: triedToUnlock ? + isWindows ? localize('overwriteElevated', "Overwrite as Admin...") : localize('overwriteElevatedSudo', "Overwrite as Sudo...") : + isWindows ? localize('saveElevated', "Retry as Admin...") : localize('saveElevatedSudo', "Retry as Sudo..."), + run: () => { + this.save({ writeElevated: true, writeUnlock: triedToUnlock, reason: SaveReason.EXPLICIT }); + } + })); + } + + // Unlock + else if (isWriteLocked) { + primaryActions.push(toAction({ id: 'fileWorkingCopy.unlock', label: localize('overwrite', "Overwrite"), run: () => this.save({ writeUnlock: true, reason: SaveReason.EXPLICIT }) })); + } + + // Retry + else { + primaryActions.push(toAction({ id: 'fileWorkingCopy.retry', label: localize('retry', "Retry"), run: () => this.save({ reason: SaveReason.EXPLICIT }) })); + } + + // Save As + primaryActions.push(toAction({ + id: 'fileWorkingCopy.saveAs', + label: localize('saveAs', "Save As..."), + run: () => { + const editor = this.workingCopyEditorService.findEditor(this); + if (editor) { + this.editorService.save(editor, { saveAs: true, reason: SaveReason.EXPLICIT }); + } + } + })); + + // Discard + primaryActions.push(toAction({ id: 'fileWorkingCopy.revert', label: localize('discard', "Discard"), run: () => this.revert() })); + + // Message + if (isWriteLocked) { + if (triedToUnlock && canSaveElevated) { + message = isWindows ? + localize('readonlySaveErrorAdmin', "Failed to save '{0}': File is read-only. Select 'Overwrite as Admin' to retry as administrator.", this.name) : + localize('readonlySaveErrorSudo', "Failed to save '{0}': File is read-only. Select 'Overwrite as Sudo' to retry as superuser.", this.name); + } else { + message = localize('readonlySaveError', "Failed to save '{0}': File is read-only. Select 'Overwrite' to attempt to make it writeable.", this.name); + } + } else if (canSaveElevated && isPermissionDenied) { + message = isWindows ? + localize('permissionDeniedSaveError', "Failed to save '{0}': Insufficient permissions. Select 'Retry as Admin' to retry as administrator.", this.name) : + localize('permissionDeniedSaveErrorSudo', "Failed to save '{0}': Insufficient permissions. Select 'Retry as Sudo' to retry as superuser.", this.name); + } else { + message = localize({ key: 'genericSaveError', comment: ['{0} is the resource that failed to save and {1} the error message'] }, "Failed to save '{0}': {1}", this.name, toErrorMessage(error, false)); + } + } + + // Show to the user as notification + const handle = this.notificationService.notify({ id: `${hash(this.resource.toString())}`, severity: Severity.Error, message, actions: { primary: primaryActions } }); + + // Remove automatically when we get saved/reverted + const listener = Event.once(Event.any(this.onDidSave, this.onDidRevert))(() => handle.close()); + Event.once(handle.onDidClose)(() => listener.dispose()); + } + + private updateLastResolvedFileStat(newFileStat: IFileStatWithMetadata): void { + const oldReadonly = this.isReadonly(); + + // First resolve - just take + if (!this.lastResolvedFileStat) { + this.lastResolvedFileStat = newFileStat; + } + + // Subsequent resolve - make sure that we only assign it if the mtime + // is equal or has advanced. + // This prevents race conditions from resolving and saving. If a save + // comes in late after a revert was called, the mtime could be out of + // sync. + else if (this.lastResolvedFileStat.mtime <= newFileStat.mtime) { + this.lastResolvedFileStat = newFileStat; + } + + // Signal that the readonly state changed + if (this.isReadonly() !== oldReadonly) { + this._onDidChangeReadonly.fire(); + } + } + + //#endregion + + //#region Revert + + async revert(options?: IRevertOptions): Promise { + if (!this.isResolved() || (!this.dirty && !options?.force)) { + return; // ignore if not resolved or not dirty and not enforced + } + + this.trace('[stored file working copy] revert()'); + + // Unset flags + const wasDirty = this.dirty; + const undoSetDirty = this.doSetDirty(false); + + // Force read from disk unless reverting soft + const softUndo = options?.soft; + if (!softUndo) { + try { + await this.resolve({ forceReadFromFile: true }); + } catch (error) { + + // FileNotFound means the file got deleted meanwhile, so ignore it + if ((error as FileOperationError).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) { + + // Set flags back to previous values, we are still dirty if revert failed + undoSetDirty(); + + throw error; + } + } + } + + // Emit file change event + this._onDidRevert.fire(); + + // Emit dirty change event + if (wasDirty) { + this._onDidChangeDirty.fire(); + } + } + + //#endregion + + //#region State + + private inConflictMode = false; + private inErrorMode = false; + + hasState(state: StoredFileWorkingCopyState): boolean { + switch (state) { + case StoredFileWorkingCopyState.CONFLICT: + return this.inConflictMode; + case StoredFileWorkingCopyState.DIRTY: + return this.dirty; + case StoredFileWorkingCopyState.ERROR: + return this.inErrorMode; + case StoredFileWorkingCopyState.ORPHAN: + return this.isOrphaned(); + case StoredFileWorkingCopyState.PENDING_SAVE: + return this.saveSequentializer.hasPending(); + case StoredFileWorkingCopyState.SAVED: + return !this.dirty; + } + } + + joinState(state: StoredFileWorkingCopyState.PENDING_SAVE): Promise { + return this.saveSequentializer.pending ?? Promise.resolve(); + } + + //#endregion + + //#region Utilities + + isReadonly(): boolean { + return this.lastResolvedFileStat?.readonly || this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); + } + + private trace(msg: string): void { + this.logService.trace(msg, this.resource.toString(true), this.typeId); + } + + //#endregion + + //#region Dispose + + override dispose(): void { + this.trace('[stored file working copy] dispose()'); + + // State + this.inConflictMode = false; + this.inErrorMode = false; + + super.dispose(); + } + + //#endregion + + //#region Remainders of text file model world (TODO@bpasero callers have to be handled in a generic way) + + private isTextFileModel(model: unknown): model is ITextFileEditorModel { + const textFileModel = this.textFileService.files.get(this.resource); + + return !!(textFileModel && this.model && (textFileModel as unknown) === (this.model as unknown)); + } + + //#endregion +} diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts new file mode 100644 index 0000000000..b6108c73f5 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts @@ -0,0 +1,585 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Event, Emitter } from 'vs/base/common/event'; +import { StoredFileWorkingCopy, StoredFileWorkingCopyState, IStoredFileWorkingCopy, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelFactory, IStoredFileWorkingCopyResolveOptions } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; +import { SaveReason } from 'vs/workbench/common/editor'; +import { ResourceMap } from 'vs/base/common/map'; +import { Promises, ResourceQueue } from 'vs/base/common/async'; +import { FileChangesEvent, FileChangeType, FileOperation, IFileService, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent } from 'vs/platform/files/common/files'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { VSBufferReadableStream } from 'vs/base/common/buffer'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { ILogService } from 'vs/platform/log/common/log'; +import { joinPath } from 'vs/base/common/resources'; +import { IWorkingCopyFileService, WorkingCopyFileEvent } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { BaseFileWorkingCopyManager, IBaseFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IElevatedFileService } from 'vs/workbench/services/files/common/elevatedFileService'; +import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; + +/** + * The only one that should be dealing with `IStoredFileWorkingCopy` and handle all + * operations that are working copy related, such as save/revert, backup + * and resolving. + */ +export interface IStoredFileWorkingCopyManager extends IBaseFileWorkingCopyManager> { + + /** + * An event for when a stored file working copy was resolved. + */ + readonly onDidResolve: Event>; + + /** + * An event for when a stored file working copy changed it's dirty state. + */ + readonly onDidChangeDirty: Event>; + + /** + * An event for when a stored file working copy failed to save. + */ + readonly onDidSaveError: Event>; + + /** + * An event for when a stored file working copy successfully saved. + */ + readonly onDidSave: Event>; + + /** + * An event for when a stored file working copy was reverted. + */ + readonly onDidRevert: Event>; + + /** + * Allows to resolve a stored file working copy. If the manager already knows + * about a stored file working copy with the same `URI`, it will return that + * existing stored file working copy. There will never be more than one + * stored file working copy per `URI` until the stored file working copy is + * disposed. + * + * Use the `IStoredFileWorkingCopyResolveOptions.reload` option to control the + * behaviour for when a stored file working copy was previously already resolved + * with regards to resolving it again from the underlying file resource + * or not. + * + * Note: Callers must `dispose` the working copy when no longer needed. + * + * @param resource used as unique identifier of the stored file working copy in + * case one is already known for this `URI`. + * @param options + */ + resolve(resource: URI, options?: IStoredFileWorkingCopyManagerResolveOptions): Promise>; + + /** + * Waits for the stored file working copy to be ready to be disposed. There may be + * conditions under which the stored file working copy cannot be disposed, e.g. when + * it is dirty. Once the promise is settled, it is safe to dispose. + */ + canDispose(workingCopy: IStoredFileWorkingCopy): true | Promise; +} + +export interface IStoredFileWorkingCopySaveEvent { + + /** + * The stored file working copy that was successfully saved. + */ + workingCopy: IStoredFileWorkingCopy; + + /** + * The reason why the stored file working copy was saved. + */ + reason: SaveReason; +} + +export interface IStoredFileWorkingCopyManagerResolveOptions extends IStoredFileWorkingCopyResolveOptions { + + /** + * If the stored file working copy was already resolved before, + * allows to trigger a reload of it to fetch the latest contents: + * - async: resolve() will return immediately and trigger + * a reload that will run in the background. + * - sync: resolve() will only return resolved when the + * stored file working copy has finished reloading. + */ + reload?: { + async: boolean + }; +} + +export class StoredFileWorkingCopyManager extends BaseFileWorkingCopyManager> implements IStoredFileWorkingCopyManager { + + //#region Events + + private readonly _onDidResolve = this._register(new Emitter>()); + readonly onDidResolve = this._onDidResolve.event; + + private readonly _onDidChangeDirty = this._register(new Emitter>()); + readonly onDidChangeDirty = this._onDidChangeDirty.event; + + private readonly _onDidSaveError = this._register(new Emitter>()); + readonly onDidSaveError = this._onDidSaveError.event; + + private readonly _onDidSave = this._register(new Emitter>()); + readonly onDidSave = this._onDidSave.event; + + private readonly _onDidRevert = this._register(new Emitter>()); + readonly onDidRevert = this._onDidRevert.event; + + //#endregion + + private readonly mapResourceToWorkingCopyListeners = new ResourceMap(); + private readonly mapResourceToPendingWorkingCopyResolve = new ResourceMap>(); + + private readonly workingCopyResolveQueue = this._register(new ResourceQueue()); + + constructor( + private readonly workingCopyTypeId: string, + private readonly modelFactory: IStoredFileWorkingCopyModelFactory, + @IFileService fileService: IFileService, + @ILifecycleService private readonly lifecycleService: ILifecycleService, + @ILabelService private readonly labelService: ILabelService, + @ILogService logService: ILogService, + @IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService, + @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @ITextFileService private readonly textFileService: ITextFileService, + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, + @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, + @INotificationService private readonly notificationService: INotificationService, + @IWorkingCopyEditorService private readonly workingCopyEditorService: IWorkingCopyEditorService, + @IEditorService private readonly editorService: IEditorService, + @IElevatedFileService private readonly elevatedFileService: IElevatedFileService + ) { + super(fileService, logService, workingCopyBackupService); + + this.registerListeners(); + } + + private registerListeners(): void { + + // Update working copies from file change events + this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e))); + + // File system provider changes + this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidChangeFileSystemProviderCapabilities(e))); + this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidChangeFileSystemProviderRegistrations(e))); + + // Working copy operations + this._register(this.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => this.onWillRunWorkingCopyFileOperation(e))); + this._register(this.workingCopyFileService.onDidFailWorkingCopyFileOperation(e => this.onDidFailWorkingCopyFileOperation(e))); + this._register(this.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => this.onDidRunWorkingCopyFileOperation(e))); + + // Lifecycle + this.lifecycleService.onWillShutdown(event => event.join(this.onWillShutdown(), 'join.fileWorkingCopyManager')); + } + + private async onWillShutdown(): Promise { + let fileWorkingCopies: IStoredFileWorkingCopy[]; + + // As long as stored file working copies are pending to be saved, we prolong the shutdown + // until that has happened to ensure we are not shutting down in the middle of + // writing to the working copy (https://github.com/microsoft/vscode/issues/116600). + while ((fileWorkingCopies = this.workingCopies.filter(workingCopy => workingCopy.hasState(StoredFileWorkingCopyState.PENDING_SAVE))).length > 0) { + await Promises.settled(fileWorkingCopies.map(workingCopy => workingCopy.joinState(StoredFileWorkingCopyState.PENDING_SAVE))); + } + } + + //#region Resolve from file or file provider changes + + private onDidChangeFileSystemProviderCapabilities(e: IFileSystemProviderCapabilitiesChangeEvent): void { + + // Resolve working copies again for file systems that changed + // capabilities to fetch latest metadata (e.g. readonly) + // into all working copies. + this.queueWorkingCopyResolves(e.scheme); + } + + private onDidChangeFileSystemProviderRegistrations(e: IFileSystemProviderRegistrationEvent): void { + if (!e.added) { + return; // only if added + } + + // Resolve working copies again for file systems that registered + // to account for capability changes: extensions may unregister + // and register the same provider with different capabilities, + // so we want to ensure to fetch latest metadata (e.g. readonly) + // into all working copies. + this.queueWorkingCopyResolves(e.scheme); + } + + private onDidFilesChange(e: FileChangesEvent): void { + + // Trigger a resolve for any update or add event that impacts + // the working copy. We also consider the added event + // because it could be that a file was added and updated + // right after. + this.queueWorkingCopyResolves(e); + } + + private queueWorkingCopyResolves(scheme: string): void; + private queueWorkingCopyResolves(e: FileChangesEvent): void; + private queueWorkingCopyResolves(schemeOrEvent: string | FileChangesEvent): void { + for (const workingCopy of this.workingCopies) { + if (workingCopy.isDirty() || !workingCopy.isResolved()) { + continue; // require a resolved, saved working copy to continue + } + + let resolveWorkingCopy = false; + if (typeof schemeOrEvent === 'string') { + resolveWorkingCopy = schemeOrEvent === workingCopy.resource.scheme; + } else { + resolveWorkingCopy = schemeOrEvent.contains(workingCopy.resource, FileChangeType.UPDATED, FileChangeType.ADDED); + } + + if (resolveWorkingCopy) { + this.queueWorkingCopyResolve(workingCopy); + } + } + } + + private queueWorkingCopyResolve(workingCopy: IStoredFileWorkingCopy): void { + + // Resolves a working copy to update (use a queue to prevent accumulation of + // resolve when the resolving actually takes long. At most we only want the + // queue to have a size of 2 (1 running resolve and 1 queued resolve). + const queue = this.workingCopyResolveQueue.queueFor(workingCopy.resource); + if (queue.size <= 1) { + queue.queue(async () => { + try { + await workingCopy.resolve(); + } catch (error) { + this.logService.error(error); + } + }); + } + } + + //#endregion + + //#region Working Copy File Events + + private readonly mapCorrelationIdToWorkingCopiesToRestore = new Map(); + + private onWillRunWorkingCopyFileOperation(e: WorkingCopyFileEvent): void { + + // Move / Copy: remember working copies to restore after the operation + if (e.operation === FileOperation.MOVE || e.operation === FileOperation.COPY) { + e.waitUntil((async () => { + const workingCopiesToRestore: { source: URI, target: URI, snapshot?: VSBufferReadableStream; }[] = []; + + for (const { source, target } of e.files) { + if (source) { + if (this.uriIdentityService.extUri.isEqual(source, target)) { + continue; // ignore if resources are considered equal + } + + // Find all working copies that related to source (can be many if resource is a folder) + const sourceWorkingCopies: IStoredFileWorkingCopy[] = []; + for (const workingCopy of this.workingCopies) { + if (this.uriIdentityService.extUri.isEqualOrParent(workingCopy.resource, source)) { + sourceWorkingCopies.push(workingCopy); + } + } + + // Remember each source working copy to load again after move is done + // with optional content to restore if it was dirty + for (const sourceWorkingCopy of sourceWorkingCopies) { + const sourceResource = sourceWorkingCopy.resource; + + // If the source is the actual working copy, just use target as new resource + let targetResource: URI; + if (this.uriIdentityService.extUri.isEqual(sourceResource, source)) { + targetResource = target; + } + + // Otherwise a parent folder of the source is being moved, so we need + // to compute the target resource based on that + else { + targetResource = joinPath(target, sourceResource.path.substr(source.path.length + 1)); + } + + workingCopiesToRestore.push({ + source: sourceResource, + target: targetResource, + snapshot: sourceWorkingCopy.isDirty() ? await sourceWorkingCopy.model?.snapshot(CancellationToken.None) : undefined + }); + } + } + } + + this.mapCorrelationIdToWorkingCopiesToRestore.set(e.correlationId, workingCopiesToRestore); + })()); + } + } + + private onDidFailWorkingCopyFileOperation(e: WorkingCopyFileEvent): void { + + // Move / Copy: restore dirty flag on working copies to restore that were dirty + if ((e.operation === FileOperation.MOVE || e.operation === FileOperation.COPY)) { + const workingCopiesToRestore = this.mapCorrelationIdToWorkingCopiesToRestore.get(e.correlationId); + if (workingCopiesToRestore) { + this.mapCorrelationIdToWorkingCopiesToRestore.delete(e.correlationId); + + workingCopiesToRestore.forEach(workingCopy => { + + // Snapshot presence means this working copy used to be dirty and so we restore that + // flag. we do NOT have to restore the content because the working copy was only soft + // reverted and did not loose its original dirty contents. + if (workingCopy.snapshot) { + this.get(workingCopy.source)?.markDirty(); + } + }); + } + } + } + + private onDidRunWorkingCopyFileOperation(e: WorkingCopyFileEvent): void { + switch (e.operation) { + + // Create: Revert existing working copies + case FileOperation.CREATE: + e.waitUntil((async () => { + for (const { target } of e.files) { + const workingCopy = this.get(target); + if (workingCopy && !workingCopy.isDisposed()) { + await workingCopy.revert(); + } + } + })()); + break; + + // Move/Copy: restore working copies that were loaded before the operation took place + case FileOperation.MOVE: + case FileOperation.COPY: + e.waitUntil((async () => { + const workingCopiesToRestore = this.mapCorrelationIdToWorkingCopiesToRestore.get(e.correlationId); + if (workingCopiesToRestore) { + this.mapCorrelationIdToWorkingCopiesToRestore.delete(e.correlationId); + + await Promises.settled(workingCopiesToRestore.map(async workingCopyToRestore => { + + // Restore the working copy at the target. if we have previous dirty content, we pass it + // over to be used, otherwise we force a reload from disk. this is important + // because we know the file has changed on disk after the move and the working copy might + // have still existed with the previous state. this ensures that the working copy is not + // tracking a stale state. + await this.resolve(workingCopyToRestore.target, { + reload: { async: false }, // enforce a reload + contents: workingCopyToRestore.snapshot + }); + })); + } + })()); + break; + } + } + + //#endregion + + //#region Resolve + + async resolve(resource: URI, options?: IStoredFileWorkingCopyManagerResolveOptions): Promise> { + + // Await a pending working copy resolve first before proceeding + // to ensure that we never resolve a working copy more than once + // in parallel + const pendingResolve = this.joinPendingResolve(resource); + if (pendingResolve) { + await pendingResolve; + } + + let workingCopyResolve: Promise; + let workingCopy = this.get(resource); + let didCreateWorkingCopy = false; + + // Working copy exists + if (workingCopy) { + + // Always reload if contents are provided + if (options?.contents) { + workingCopyResolve = workingCopy.resolve(options); + } + + // Reload async or sync based on options + else if (options?.reload) { + + // Async reload: trigger a reload but return immediately + if (options.reload.async) { + workingCopy.resolve(options); + workingCopyResolve = Promise.resolve(); + } + + // Sync reload: do not return until working copy reloaded + else { + workingCopyResolve = workingCopy.resolve(options); + } + } + + // Do not reload + else { + workingCopyResolve = Promise.resolve(); + } + } + + // Stored file working copy does not exist + else { + didCreateWorkingCopy = true; + + workingCopy = new StoredFileWorkingCopy( + this.workingCopyTypeId, + resource, + this.labelService.getUriBasenameLabel(resource), + this.modelFactory, + this.fileService, this.logService, this.textFileService, this.filesConfigurationService, + this.workingCopyBackupService, this.workingCopyService, this.notificationService, this.workingCopyEditorService, + this.editorService, this.elevatedFileService + ); + + workingCopyResolve = workingCopy.resolve(options); + + this.registerWorkingCopy(workingCopy); + } + + // Store pending resolve to avoid race conditions + this.mapResourceToPendingWorkingCopyResolve.set(resource, workingCopyResolve); + + // Make known to manager (if not already known) + this.add(resource, workingCopy); + + // Emit some events if we created the working copy + if (didCreateWorkingCopy) { + + // If the working copy is dirty right from the beginning, + // make sure to emit this as an event + if (workingCopy.isDirty()) { + this._onDidChangeDirty.fire(workingCopy); + } + } + + try { + + // Wait for working copy to resolve + await workingCopyResolve; + + // Remove from pending resolves + this.mapResourceToPendingWorkingCopyResolve.delete(resource); + + // Stored file working copy can be dirty if a backup was restored, so we make sure to + // have this event delivered if we created the working copy here + if (didCreateWorkingCopy && workingCopy.isDirty()) { + this._onDidChangeDirty.fire(workingCopy); + } + + return workingCopy; + } catch (error) { + + // Free resources of this invalid working copy + if (workingCopy) { + workingCopy.dispose(); + } + + // Remove from pending resolves + this.mapResourceToPendingWorkingCopyResolve.delete(resource); + + throw error; + } + } + + private joinPendingResolve(resource: URI): Promise | undefined { + const pendingWorkingCopyResolve = this.mapResourceToPendingWorkingCopyResolve.get(resource); + if (pendingWorkingCopyResolve) { + return pendingWorkingCopyResolve.then(undefined, error => {/* ignore any error here, it will bubble to the original requestor*/ }); + } + + return undefined; + } + + private registerWorkingCopy(workingCopy: IStoredFileWorkingCopy): void { + + // Install working copy listeners + const workingCopyListeners = new DisposableStore(); + workingCopyListeners.add(workingCopy.onDidResolve(() => this._onDidResolve.fire(workingCopy))); + workingCopyListeners.add(workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(workingCopy))); + workingCopyListeners.add(workingCopy.onDidSaveError(() => this._onDidSaveError.fire(workingCopy))); + workingCopyListeners.add(workingCopy.onDidSave(reason => this._onDidSave.fire({ workingCopy: workingCopy, reason }))); + workingCopyListeners.add(workingCopy.onDidRevert(() => this._onDidRevert.fire(workingCopy))); + + // Keep for disposal + this.mapResourceToWorkingCopyListeners.set(workingCopy.resource, workingCopyListeners); + } + + protected override remove(resource: URI): void { + super.remove(resource); + + // Dispose any exsting working copy listeners + const workingCopyListener = this.mapResourceToWorkingCopyListeners.get(resource); + if (workingCopyListener) { + dispose(workingCopyListener); + this.mapResourceToWorkingCopyListeners.delete(resource); + } + } + + //#endregion + + //#region Lifecycle + + canDispose(workingCopy: IStoredFileWorkingCopy): true | Promise { + + // Quick return if working copy already disposed or not dirty and not resolving + if ( + workingCopy.isDisposed() || + (!this.mapResourceToPendingWorkingCopyResolve.has(workingCopy.resource) && !workingCopy.isDirty()) + ) { + return true; + } + + // Promise based return in all other cases + return this.doCanDispose(workingCopy); + } + + private async doCanDispose(workingCopy: IStoredFileWorkingCopy): Promise { + + // If we have a pending working copy resolve, await it first and then try again + const pendingResolve = this.joinPendingResolve(workingCopy.resource); + if (pendingResolve) { + await pendingResolve; + + return this.canDispose(workingCopy); + } + + // Dirty working copy: we do not allow to dispose dirty working copys + // to prevent data loss cases. dirty working copys can only be disposed when + // they are either saved or reverted + if (workingCopy.isDirty()) { + await Event.toPromise(workingCopy.onDidChangeDirty); + + return this.canDispose(workingCopy); + } + + return true; + } + + override dispose(): void { + super.dispose(); + + // Clear pending working copy resolves + this.mapResourceToPendingWorkingCopyResolve.clear(); + + // Dispose the working copy change listeners + dispose(this.mapResourceToWorkingCopyListeners.values()); + this.mapResourceToWorkingCopyListeners.clear(); + } + + //#endregion +} diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts new file mode 100644 index 0000000000..ea4c92e4ee --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -0,0 +1,284 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event, Emitter } from 'vs/base/common/event'; +import { VSBufferReadableStream } from 'vs/base/common/buffer'; +import { IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { ISaveOptions } from 'vs/workbench/common/editor'; +import { raceCancellation } from 'vs/base/common/async'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { emptyStream } from 'vs/base/common/stream'; + +/** + * Untitled file specific working copy model factory. + */ +export interface IUntitledFileWorkingCopyModelFactory extends IFileWorkingCopyModelFactory { } + +/** + * The underlying model of a untitled file working copy provides + * some methods for the untitled file working copy to function. + * The model is typically only available after the working copy + * has been resolved via it's `resolve()` method. + */ +export interface IUntitledFileWorkingCopyModel extends IFileWorkingCopyModel { + + readonly onDidChangeContent: Event; +} + +export interface IUntitledFileWorkingCopyModelContentChangedEvent { + + /** + * Flag that indicates that the content change + * resulted in empty contents. A untitled file + * working copy without contents may be marked + * as non-dirty. + */ + readonly isEmpty: boolean; +} + +export interface IUntitledFileWorkingCopy extends IFileWorkingCopy { + + /** + * Whether this untitled file working copy model has an associated file path. + */ + readonly hasAssociatedFilePath: boolean; + + /** + * Whether we have a resolved model or not. + */ + isResolved(): this is IResolvedUntitledFileWorkingCopy; +} + +export interface IResolvedUntitledFileWorkingCopy extends IUntitledFileWorkingCopy { + + /** + * A resolved untitled file working copy has a resolved model. + */ + readonly model: M; +} + +export interface IUntitledFileWorkingCopySaveDelegate { + + /** + * A delegate to enable saving of untitled file working copies. + */ + (workingCopy: IUntitledFileWorkingCopy, options?: ISaveOptions): Promise; +} + +export class UntitledFileWorkingCopy extends Disposable implements IUntitledFileWorkingCopy { + + readonly capabilities = WorkingCopyCapabilities.Untitled; + + private _model: M | undefined = undefined; + get model(): M | undefined { return this._model; } + + //#region Events + + private readonly _onDidChangeContent = this._register(new Emitter()); + readonly onDidChangeContent = this._onDidChangeContent.event; + + private readonly _onDidChangeDirty = this._register(new Emitter()); + readonly onDidChangeDirty = this._onDidChangeDirty.event; + + private readonly _onDidRevert = this._register(new Emitter()); + readonly onDidRevert = this._onDidRevert.event; + + private readonly _onWillDispose = this._register(new Emitter()); + readonly onWillDispose = this._onWillDispose.event; + + //#endregion + + constructor( + readonly typeId: string, + readonly resource: URI, + readonly name: string, + readonly hasAssociatedFilePath: boolean, + private readonly initialValue: VSBufferReadableStream | undefined, + private readonly modelFactory: IUntitledFileWorkingCopyModelFactory, + private readonly saveDelegate: IUntitledFileWorkingCopySaveDelegate, + @IWorkingCopyService workingCopyService: IWorkingCopyService, + @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, + @ILogService private readonly logService: ILogService + ) { + super(); + + // Make known to working copy service + this._register(workingCopyService.registerWorkingCopy(this)); + } + + //#region Dirty + + private dirty = this.hasAssociatedFilePath || !!this.initialValue; + + isDirty(): boolean { + return this.dirty; + } + + private setDirty(dirty: boolean): void { + if (this.dirty === dirty) { + return; + } + + this.dirty = dirty; + this._onDidChangeDirty.fire(); + } + + //#endregion + + + //#region Resolve + + async resolve(): Promise { + this.trace('[untitled file working copy] resolve()'); + + if (this.isResolved()) { + this.trace('[untitled file working copy] resolve() - exit (already resolved)'); + + // return early if the untitled file working copy is already + // resolved assuming that the contents have meanwhile changed + // in the underlying model. we only resolve untitled once. + return; + } + + let untitledContents: VSBufferReadableStream; + + // Check for backups or use initial value or empty + let specificThis = (this as UntitledFileWorkingCopy); // {{SQL CARBON EDIT}} Fix type assertion error from predicate function + const backup = await specificThis.workingCopyBackupService.resolve(this); + if (backup) { + specificThis.trace('[untitled file working copy] resolve() - with backup'); + + untitledContents = backup.value; + } else if (specificThis.initialValue) { + specificThis.trace('[untitled file working copy] resolve() - with initial contents'); + + untitledContents = specificThis.initialValue; + } else { + specificThis.trace('[untitled file working copy] resolve() - empty'); + + untitledContents = emptyStream(); + } + + // Create model + await specificThis.doCreateModel(untitledContents); + + // Untitled associated to file path are dirty right away as well as untitled with content + specificThis.setDirty(specificThis.hasAssociatedFilePath || !!backup || !!specificThis.initialValue); + + // If we have initial contents, make sure to emit this + // as the appropiate events to the outside. + if (!!backup || specificThis.initialValue) { + specificThis._onDidChangeContent.fire(); + } + } + + private async doCreateModel(contents: VSBufferReadableStream): Promise { + this.trace('[untitled file working copy] doCreateModel()'); + + // Create model and dispose it when we get disposed + this._model = this._register(await this.modelFactory.createModel(this.resource, contents, CancellationToken.None)); + + // Model listeners + this.installModelListeners(this._model); + } + + private installModelListeners(model: M): void { + + // Content Change + this._register(model.onDidChangeContent(e => this.onModelContentChanged(e))); + + // Lifecycle + this._register(model.onWillDispose(() => this.dispose())); + } + + private onModelContentChanged(e: IUntitledFileWorkingCopyModelContentChangedEvent): void { + + // Mark the untitled file working copy as non-dirty once its + // content becomes empty and we do not have an associated + // path set. we never want dirty indicator in that case. + if (!this.hasAssociatedFilePath && e.isEmpty) { + this.setDirty(false); + } + + // Turn dirty otherwise + else { + this.setDirty(true); + } + + // Emit as general content change event + this._onDidChangeContent.fire(); + } + + isResolved(): this is IResolvedUntitledFileWorkingCopy { + return !!this.model; + } + + //#endregion + + + //#region Backup + + async backup(token: CancellationToken): Promise { + + // Fill in content if we are resolved + let content: VSBufferReadableStream | undefined = undefined; + if (this.isResolved()) { + content = await raceCancellation(this.model.snapshot(token), token); + } + + return { content }; + } + + //#endregion + + + //#region Save + + save(options?: ISaveOptions): Promise { + this.trace('[untitled file working copy] save()'); + + return this.saveDelegate(this, options); + } + + //#endregion + + + //#region Revert + + async revert(): Promise { + this.trace('[untitled file working copy] revert()'); + + // No longer dirty + this.setDirty(false); + + // Emit as event + this._onDidRevert.fire(); + + // A reverted untitled file working copy is invalid + // because it has no actual source on disk to revert to. + // As such we dispose the model. + this.dispose(); + } + + //#endregion + + override dispose(): void { + this.trace('[untitled file working copy] dispose()'); + + this._onWillDispose.fire(); + + super.dispose(); + } + + private trace(msg: string): void { + this.logService.trace(msg, this.resource.toString(true), this.typeId); + } +} diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts new file mode 100644 index 0000000000..f3ac6a927b --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -0,0 +1,256 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBufferReadableStream } from 'vs/base/common/buffer'; +import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelFactory, IUntitledFileWorkingCopySaveDelegate, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; +import { Event, Emitter } from 'vs/base/common/event'; +import { Schemas } from 'vs/base/common/network'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { IFileService } from 'vs/platform/files/common/files'; +import { BaseFileWorkingCopyManager, IBaseFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; +import { ResourceMap } from 'vs/base/common/map'; + +/** + * The only one that should be dealing with `IUntitledFileWorkingCopy` and + * handle all operations that are working copy related, such as save/revert, + * backup and resolving. + */ +export interface IUntitledFileWorkingCopyManager extends IBaseFileWorkingCopyManager> { + + /** + * An event for when a untitled file working copy changed it's dirty state. + */ + readonly onDidChangeDirty: Event>; + + /** + * An event for when a untitled file working copy is about to be disposed. + */ + readonly onWillDispose: Event>; + + /** + * Create a new untitled file working copy with optional initial contents. + * + * Note: Callers must `dispose` the working copy when no longer needed. + */ + resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; + + /** + * Create a new untitled file working copy with optional initial contents + * and associated resource. The associated resource will be used when + * saving and will not require to ask the user for a file path. + * + * Note: Callers must `dispose` the working copy when no longer needed. + */ + resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; + + /** + * Creates a new untitled file working copy with optional initial contents + * with the provided resource or return an existing untitled file working + * copy otherwise. + * + * Note: Callers must `dispose` the working copy when no longer needed. + */ + resolve(options?: INewOrExistingUntitledFileWorkingCopyOptions): Promise>; +} + +export interface INewUntitledFileWorkingCopyOptions { + + /** + * Initial value of the untitled file working copy. + * + * Note: An untitled file working copy with initial + * value is dirty right from the beginning. + */ + contents?: VSBufferReadableStream; +} + +export interface INewUntitledFileWorkingCopyWithAssociatedResourceOptions extends INewUntitledFileWorkingCopyOptions { + + /** + * Resource components to associate with the untitled file working copy. + * When saving, the associated components will be used and the user + * is not being asked to provide a file path. + * + * Note: currently it is not possible to specify the `scheme` to use. The + * untitled file working copy will saved to the default local or remote resource. + */ + associatedResource: { authority?: string; path?: string; query?: string; fragment?: string; } +} + +export interface INewOrExistingUntitledFileWorkingCopyOptions extends INewUntitledFileWorkingCopyOptions { + + /** + * A resource to identify the untitled file working copy + * to create or return if already existing. + * + * Note: the resource will not be used unless the scheme is `untitled`. + */ + untitledResource: URI; +} + +type IInternalUntitledFileWorkingCopyOptions = INewUntitledFileWorkingCopyOptions & INewUntitledFileWorkingCopyWithAssociatedResourceOptions & INewOrExistingUntitledFileWorkingCopyOptions; + +export class UntitledFileWorkingCopyManager extends BaseFileWorkingCopyManager> implements IUntitledFileWorkingCopyManager { + + //#region Events + + private readonly _onDidChangeDirty = this._register(new Emitter>()); + readonly onDidChangeDirty = this._onDidChangeDirty.event; + + private readonly _onWillDispose = this._register(new Emitter>()); + readonly onWillDispose = this._onWillDispose.event; + + //#endregion + + private readonly mapResourceToWorkingCopyListeners = new ResourceMap(); + + constructor( + private readonly workingCopyTypeId: string, + private readonly modelFactory: IUntitledFileWorkingCopyModelFactory, + private readonly saveDelegate: IUntitledFileWorkingCopySaveDelegate, + @IFileService fileService: IFileService, + @ILabelService private readonly labelService: ILabelService, + @ILogService logService: ILogService, + @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, + @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService + ) { + super(fileService, logService, workingCopyBackupService); + } + + //#region Resolve + + resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; + resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; + resolve(options?: INewOrExistingUntitledFileWorkingCopyOptions): Promise>; + async resolve(options?: IInternalUntitledFileWorkingCopyOptions): Promise> { + const workingCopy = this.doCreateOrGet(options); + await workingCopy.resolve(); + + return workingCopy; + } + + private doCreateOrGet(options: IInternalUntitledFileWorkingCopyOptions = Object.create(null)): IUntitledFileWorkingCopy { + const massagedOptions = this.massageOptions(options); + + // Return existing instance if asked for it + if (massagedOptions.untitledResource) { + const existingWorkingCopy = this.get(massagedOptions.untitledResource); + if (existingWorkingCopy) { + return existingWorkingCopy; + } + } + + // Create new instance otherwise + return this.doCreate(massagedOptions); + } + + private massageOptions(options: IInternalUntitledFileWorkingCopyOptions): IInternalUntitledFileWorkingCopyOptions { + const massagedOptions: IInternalUntitledFileWorkingCopyOptions = Object.create(null); + + // Handle associcated resource + if (options.associatedResource) { + massagedOptions.untitledResource = URI.from({ + scheme: Schemas.untitled, + authority: options.associatedResource.authority, + fragment: options.associatedResource.fragment, + path: options.associatedResource.path, + query: options.associatedResource.query + }); + massagedOptions.associatedResource = options.associatedResource; + } + + // Handle untitled resource + else if (options.untitledResource?.scheme === Schemas.untitled) { + massagedOptions.untitledResource = options.untitledResource; + } + + // Take over initial value + massagedOptions.contents = options.contents; + + return massagedOptions; + } + + private doCreate(options: IInternalUntitledFileWorkingCopyOptions): IUntitledFileWorkingCopy { + + // Create a new untitled resource if none is provided + let untitledResource = options.untitledResource; + if (!untitledResource) { + let counter = 1; + do { + untitledResource = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}` }); + counter++; + } while (this.has(untitledResource)); + } + + // Create new working copy with provided options + const workingCopy = new UntitledFileWorkingCopy( + this.workingCopyTypeId, + untitledResource, + this.labelService.getUriBasenameLabel(untitledResource), + !!options.associatedResource, + options.contents, + this.modelFactory, + this.saveDelegate, + this.workingCopyService, + this.workingCopyBackupService, + this.logService + ); + + // Register + this.registerWorkingCopy(workingCopy); + + return workingCopy; + } + + private registerWorkingCopy(workingCopy: IUntitledFileWorkingCopy): void { + + // Install working copy listeners + const workingCopyListeners = new DisposableStore(); + workingCopyListeners.add(workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(workingCopy))); + workingCopyListeners.add(workingCopy.onWillDispose(() => this._onWillDispose.fire(workingCopy))); + + // Keep for disposal + this.mapResourceToWorkingCopyListeners.set(workingCopy.resource, workingCopyListeners); + + // Add to cache + this.add(workingCopy.resource, workingCopy); + + // If the working copy is dirty right from the beginning, + // make sure to emit this as an event + if (workingCopy.isDirty()) { + this._onDidChangeDirty.fire(workingCopy); + } + } + + protected override remove(resource: URI): void { + super.remove(resource); + + // Dispose any exsting working copy listeners + const workingCopyListener = this.mapResourceToWorkingCopyListeners.get(resource); + if (workingCopyListener) { + dispose(workingCopyListener); + this.mapResourceToWorkingCopyListeners.delete(resource); + } + } + + //#endregion + + //#region Lifecycle + + override dispose(): void { + super.dispose(); + + // Dispose the working copy change listeners + dispose(this.mapResourceToWorkingCopyListeners.values()); + this.mapResourceToWorkingCopyListeners.clear(); + } + + //#endregion +} diff --git a/src/vs/workbench/services/workingCopy/common/workingCopy.ts b/src/vs/workbench/services/workingCopy/common/workingCopy.ts index 419f6a1207..0ab69ceea5 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopy.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyBackup.ts b/src/vs/workbench/services/workingCopy/common/workingCopyBackup.ts index ca0f2f2e25..07eed3e08a 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyBackup.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyBackup.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -76,8 +76,8 @@ export interface IWorkingCopyBackupService { /** * Discards all working copy backups. * - * The optional set of identifiers can be provided to discard all but the - * provided ones. + * The optional set of identifiers in the filter can be + * provided to discard all but the provided ones. */ - discardBackups(except?: IWorkingCopyIdentifier[]): Promise; + discardBackups(filter?: { except: IWorkingCopyIdentifier[] }): Promise; } diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyBackupService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyBackupService.ts index 9fa7208291..9413f67ac5 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyBackupService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyBackupService.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { basename, isEqual, joinPath } from 'vs/base/common/resources'; @@ -18,12 +18,8 @@ import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Schemas } from 'vs/base/common/network'; import { hash } from 'vs/base/common/hash'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { LegacyWorkingCopyBackupRestorer } from 'vs/workbench/services/workingCopy/common/legacyBackupRestorer'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { isEmptyObject } from 'vs/base/common/types'; -import { IWorkingCopyBackupMeta, IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopyBackupMeta, IWorkingCopyIdentifier, NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy'; export class WorkingCopyBackupsModel { @@ -161,8 +157,8 @@ export abstract class WorkingCopyBackupService implements IWorkingCopyBackupServ return this.impl.discardBackup(identifier); } - discardBackups(except?: IWorkingCopyIdentifier[]): Promise { - return this.impl.discardBackups(except); + discardBackups(filter?: { except: IWorkingCopyIdentifier[] }): Promise { + return this.impl.discardBackups(filter); } getBackups(): Promise { @@ -311,21 +307,22 @@ class NativeWorkingCopyBackupServiceImpl extends Disposable implements IWorkingC return `${identifier.resource.toString()}${NativeWorkingCopyBackupServiceImpl.PREAMBLE_META_SEPARATOR}${JSON.stringify({ ...meta, typeId: identifier.typeId })}${NativeWorkingCopyBackupServiceImpl.PREAMBLE_END_MARKER}`; } - async discardBackups(except?: IWorkingCopyIdentifier[]): Promise { + async discardBackups(filter?: { except: IWorkingCopyIdentifier[] }): Promise { const model = await this.ready; // Discard all but some backups + const except = filter?.except; if (Array.isArray(except) && except.length > 0) { const exceptMap = new ResourceMap(); for (const exceptWorkingCopy of except) { exceptMap.set(this.toBackupResource(exceptWorkingCopy), true); } - for (const backupResource of model.get()) { + await Promises.settled(model.get().map(async backupResource => { if (!exceptMap.has(backupResource)) { await this.doDiscardBackup(backupResource); } - } + })); } // Discard all backups @@ -407,7 +404,7 @@ class NativeWorkingCopyBackupServiceImpl extends Disposable implements IWorkingC } return { - typeId: typeId ?? '', // Fallback for previous backups that do not encode the typeId (TODO@bpasero remove me eventually) + typeId: typeId ?? NO_TYPE_ID, resource: URI.parse(resourcePreamble) }; } @@ -535,7 +532,8 @@ export class InMemoryWorkingCopyBackupService implements IWorkingCopyBackupServi this.backups.delete(this.toBackupResource(identifier)); } - async discardBackups(except?: IWorkingCopyIdentifier[]): Promise { + async discardBackups(filter?: { except: IWorkingCopyIdentifier[] }): Promise { + const except = filter?.except; if (Array.isArray(except) && except.length > 0) { const exceptMap = new ResourceMap(); for (const exceptWorkingCopy of except) { @@ -591,6 +589,3 @@ function hashPath(resource: URI): string { function hashString(str: string): string { return hash(str).toString(16); } - -// Register Backup Restorer -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(LegacyWorkingCopyBackupRestorer, LifecyclePhase.Starting); diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyBackupTracker.ts b/src/vs/workbench/services/workingCopy/common/workingCopyBackupTracker.ts index d9e13d31ac..ba86825860 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyBackupTracker.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyBackupTracker.ts @@ -14,8 +14,7 @@ import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/ import { IWorkingCopyEditorHandler, IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; import { Promises } from 'vs/base/common/async'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorInput } from 'vs/workbench/common/editor'; -import { EditorOverride } from 'vs/platform/editor/common/editor'; +import { EditorsOrder, IEditorInput } from 'vs/workbench/common/editor'; /** * The working copy backup tracker deals with: @@ -209,7 +208,10 @@ export abstract class WorkingCopyBackupTracker extends Disposable { //#region Backup Restorer protected readonly unrestoredBackups = new Set(); - private readonly whenReady = this.resolveBackupsToRestore(); + protected readonly whenReady = this.resolveBackupsToRestore(); + + private _isReady = false; + protected get isReady(): boolean { return this._isReady; } private async resolveBackupsToRestore(): Promise { @@ -220,6 +222,8 @@ export abstract class WorkingCopyBackupTracker extends Disposable { for (const backup of await this.workingCopyBackupService.getBackups()) { this.unrestoredBackups.add(backup); } + + this._isReady = true; } protected async restoreBackups(handler: IWorkingCopyEditorHandler): Promise { @@ -243,7 +247,7 @@ export abstract class WorkingCopyBackupTracker extends Disposable { // Collect already opened editors for backup let hasOpenedEditorForBackup = false; - for (const editor of this.editorService.editors) { + for (const { editor } of this.editorService.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) { const isUnrestoredBackupOpened = handler.isOpen(unrestoredBackup, editor); if (isUnrestoredBackupOpened) { openedEditorsForBackups.push(editor); @@ -254,7 +258,7 @@ export abstract class WorkingCopyBackupTracker extends Disposable { // Otherwise, make sure to create at least one editor // for the backup to show if (!hasOpenedEditorForBackup) { - nonOpenedEditorsForBackups.push(handler.createEditor(unrestoredBackup)); + nonOpenedEditorsForBackups.push(await handler.createEditor(unrestoredBackup)); } // Remember as (potentially) restored @@ -269,8 +273,7 @@ export abstract class WorkingCopyBackupTracker extends Disposable { options: { pinned: true, preserveFocus: true, - inactive: true, - override: EditorOverride.DISABLED + inactive: true } }))); diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyEditorService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyEditorService.ts index 5a5494ae8f..52944360a4 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyEditorService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyEditorService.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; @@ -29,7 +29,7 @@ export interface IWorkingCopyEditorHandler { /** * Create an editor that is suitable of opening the provided working copy. */ - createEditor(workingCopy: IWorkingCopyIdentifier): IEditorInput; + createEditor(workingCopy: IWorkingCopyIdentifier): IEditorInput | Promise; } export interface IWorkingCopyEditorService { diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts index fa859123ae..565ee1e078 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts @@ -9,7 +9,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { Disposable, IDisposable, toDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; -import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopy, IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { Schemas } from 'vs/base/common/network'; // {{SQL CARBON EDIT}} @chlafreniere need to block working copies of notebook editors from being tracked export const IWorkingCopyService = createDecorator('workingCopyService'); @@ -91,6 +91,19 @@ export interface IWorkingCopyService { */ registerWorkingCopy(workingCopy: IWorkingCopy): IDisposable; + /** + * Whether a working copy with the given resource or identifier + * exists. + */ + has(identifier: IWorkingCopyIdentifier): boolean; + has(resource: URI): boolean; + + /** + * Returns a working copy with the given identifier or `undefined` + * if no such working copy exists. + */ + get(identifier: IWorkingCopyIdentifier): IWorkingCopy | undefined; + //#endregion } @@ -181,6 +194,20 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic } } + has(identifier: IWorkingCopyIdentifier): boolean; + has(resource: URI): boolean; + has(resourceOrIdentifier: URI | IWorkingCopyIdentifier): boolean { + if (URI.isUri(resourceOrIdentifier)) { + return this.mapResourceToWorkingCopies.has(resourceOrIdentifier); + } + + return this.mapResourceToWorkingCopies.get(resourceOrIdentifier.resource)?.has(resourceOrIdentifier.typeId) ?? false; + } + + get(identifier: IWorkingCopyIdentifier): IWorkingCopy | undefined { + return this.mapResourceToWorkingCopies.get(identifier.resource)?.get(identifier.typeId); + } + //#endregion diff --git a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts index 2134f7da7c..e89005187d 100644 --- a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts +++ b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts @@ -8,7 +8,7 @@ import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/com import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopy, IWorkingCopyIdentifier, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { ILifecycleService, ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ConfirmResult, IFileDialogService, IDialogService, getFileNamesMessage } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; @@ -24,7 +24,6 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' 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'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker implements IWorkbenchContribution { @@ -41,7 +40,6 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp @ILogService logService: ILogService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @IProgressService private readonly progressService: IProgressService, - @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IWorkingCopyEditorService workingCopyEditorService: IWorkingCopyEditorService, @IEditorService editorService: IEditorService ) { @@ -145,7 +143,7 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp ? getFileNamesMessage(dirtyWorkingCopies.map(x => x.name)) + '\n' + advice : advice; - this.dialogService.show(Severity.Error, msg, [localize('ok', 'OK')], { detail }); + this.dialogService.show(Severity.Error, msg, undefined, { detail }); this.logService.error(error ? `[backup tracker] ${msg}: ${error}` : `[backup tracker] ${msg}`); } @@ -224,7 +222,10 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp } catch (backupError) { error = backupError; } - }, localize('backupBeforeShutdown', "Waiting for dirty editors to backup...")); + }, + localize('backupBeforeShutdownMessage', "Backing up dirty editors is taking longer than expected..."), + localize('backupBeforeShutdownDetail', "Click 'Cancel' to stop waiting and to save or revert dirty editors.") + ); return { backups, error }; } @@ -293,7 +294,7 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp 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...")); + }, localize('saveBeforeShutdown', "Saving dirty editors is taking longer than expected...")); } private doRevertAllBeforeShutdown(dirtyWorkingCopies: IWorkingCopy[]): Promise { @@ -310,52 +311,66 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp // 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...")); + }, localize('revertBeforeShutdown', "Reverting dirty editors is taking longer than expected...")); } - private withProgressAndCancellation(promiseFactory: (token: CancellationToken) => Promise, title: string): Promise { + private withProgressAndCancellation(promiseFactory: (token: CancellationToken) => Promise, title: string, detail?: 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 + location: ProgressLocation.Dialog, // use a dialog to prevent the user from making any more changes now (https://github.com/microsoft/vscode/issues/122774) + cancellable: true, // allow to cancel (https://github.com/microsoft/vscode/issues/112278) + delay: 800, // delay notification so that it only appears when operation takes a long time + title, + detail }, () => raceCancellation(promiseFactory(cts.token), cts.token), () => cts.dispose(true)); } - private noVeto(backupsToDiscard: IWorkingCopy[]): boolean | Promise { - if (!this.editorGroupService.isRestored()) { - return false; // if editors have not restored, we are very likely not up to speed with backups and thus should not discard them - } + private async noVeto(backupsToDiscard: IWorkingCopyIdentifier[]): Promise { - return Promises.settled(backupsToDiscard.map(workingCopy => this.workingCopyBackupService.discardBackup(workingCopy))).then(() => false, () => false); + // Discard backups from working copies the + // user either saved or reverted + await this.discardBackupsBeforeShutdown(backupsToDiscard); + + return false; // no veto (no dirty) } private async onBeforeShutdownWithoutDirty(): Promise { - // If we have proceeded enough that editors and dirty state - // has restored, we make sure that no backups lure around - // given we have no known dirty working copy. This helps - // to clean up stale backups as for example reported in - // https://github.com/microsoft/vscode/issues/92962 - // - // However, we never want to discard backups that we know - // were not restored in the session. - if (this.editorGroupService.isRestored()) { - try { - - // Backups without `typeId` are handed in the legacy backup - // restorer still and thus we explicitly don't want to keep - // them on shutdown, otherwise they would always come back. - // TODO@bpasero remove this check once typeId has been adopted. - const backupsToKeep = Array.from(this.unrestoredBackups).filter(unrestoredBackup => unrestoredBackup.typeId.length > 0); - await this.workingCopyBackupService.discardBackups(backupsToKeep); - } catch (error) { - this.logService.error(`[backup tracker] error discarding backups: ${error}`); - } - } + // Discard all backups except those that + // were not restored + await this.discardBackupsBeforeShutdown({ except: Array.from(this.unrestoredBackups) }); return false; // no veto (no dirty) } + + private discardBackupsBeforeShutdown(backupsToDiscard: IWorkingCopyIdentifier[]): Promise; + private discardBackupsBeforeShutdown(backupsToKeep: { except: IWorkingCopyIdentifier[] }): Promise; + private async discardBackupsBeforeShutdown(arg1: IWorkingCopyIdentifier[] | { except: IWorkingCopyIdentifier[] }): Promise { + + // We never discard any backups before we are ready + // and have resolved all backups that exist. This + // is important to not loose backups that have not + // been handled. + if (!this.isReady) { + return; + } + + // When we shutdown either with no dirty working copies left + // or with some handled, we start to discard these backups + // to free them up. This helps to get rid of stale backups + // as reported in https://github.com/microsoft/vscode/issues/92962 + // + // However, we never want to discard backups that we know + // were not restored in the session. + try { + if (Array.isArray(arg1)) { + await Promises.settled(arg1.map(workingCopy => this.workingCopyBackupService.discardBackup(workingCopy))); + } else { + await this.workingCopyBackupService.discardBackups(arg1); + } + } catch (error) { + this.logService.error(`[backup tracker] error discarding backups: ${error}`); + } + } } diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts index 9370d2f4b3..dbc62cdbf3 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts @@ -1,421 +1,192 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * 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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { workbenchInstantiationService, TestServiceAccessor, TestWillShutdownEvent } from 'vs/workbench/test/browser/workbenchTestServices'; -import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; -import { IFileWorkingCopy, IFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { workbenchInstantiationService, TestServiceAccessor, TestInMemoryFileSystemProvider } from 'vs/workbench/test/browser/workbenchTestServices'; +import { StoredFileWorkingCopy, IStoredFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; -import { FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files'; -import { timeout } from 'vs/base/common/async'; -import { TestFileWorkingCopyModel, TestFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { TestStoredFileWorkingCopyModel, TestStoredFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test'; +import { Schemas } from 'vs/base/common/network'; +import { IFileWorkingCopyManager, FileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; +import { TestUntitledFileWorkingCopyModel, TestUntitledFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test'; +import { UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; suite('FileWorkingCopyManager', () => { let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; - let manager: IFileWorkingCopyManager; + let manager: IFileWorkingCopyManager; setup(() => { instantiationService = workbenchInstantiationService(); accessor = instantiationService.createInstance(TestServiceAccessor); - const factory = new TestFileWorkingCopyModelFactory(); - manager = new FileWorkingCopyManager('testWorkingCopyType', factory, accessor.fileService, accessor.lifecycleService, accessor.labelService, instantiationService, accessor.logService, accessor.fileDialogService, accessor.workingCopyFileService, accessor.uriIdentityService); + accessor.fileService.registerProvider(Schemas.file, new TestInMemoryFileSystemProvider()); + accessor.fileService.registerProvider(Schemas.vscodeRemote, new TestInMemoryFileSystemProvider()); + + manager = new FileWorkingCopyManager( + 'testFileWorkingCopyType', + new TestStoredFileWorkingCopyModelFactory(), + new TestUntitledFileWorkingCopyModelFactory(), + accessor.fileService, accessor.lifecycleService, accessor.labelService, accessor.logService, + accessor.workingCopyFileService, accessor.workingCopyBackupService, accessor.uriIdentityService, accessor.fileDialogService, + accessor.textFileService, accessor.filesConfigurationService, accessor.workingCopyService, accessor.notificationService, + accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService, accessor.pathService, + accessor.environmentService, accessor.dialogService + ); }); teardown(() => { manager.dispose(); }); - test('resolve', async () => { - const resource = URI.file('/test.html'); - - const events: IFileWorkingCopy[] = []; - const listener = manager.onDidCreate(workingCopy => { - events.push(workingCopy); + test('onDidCreate, get, workingCopies', async () => { + let createCounter = 0; + manager.onDidCreate(e => { + createCounter++; }); - const resolvePromise = manager.resolve(resource); - assert.ok(manager.get(resource)); // working copy known even before resolved() - assert.strictEqual(manager.workingCopies.length, 1); - - const workingCopy1 = await resolvePromise; - assert.ok(workingCopy1); - assert.ok(workingCopy1.model); - assert.strictEqual(workingCopy1.typeId, 'testWorkingCopyType'); - assert.strictEqual(manager.get(resource), workingCopy1); - - const workingCopy2 = await manager.resolve(resource); - assert.strictEqual(workingCopy2, workingCopy1); - assert.strictEqual(manager.workingCopies.length, 1); - workingCopy1.dispose(); - - const workingCopy3 = await manager.resolve(resource); - assert.notStrictEqual(workingCopy3, workingCopy2); - assert.strictEqual(manager.workingCopies.length, 1); - assert.strictEqual(manager.get(resource), workingCopy3); - workingCopy3.dispose(); + const fileUri = URI.file('/test.html'); assert.strictEqual(manager.workingCopies.length, 0); + assert.strictEqual(manager.get(fileUri), undefined); - assert.strictEqual(events.length, 2); - assert.strictEqual(events[0].resource.toString(), workingCopy1.resource.toString()); - assert.strictEqual(events[1].resource.toString(), workingCopy2.resource.toString()); + const fileWorkingCopy = await manager.resolve(fileUri); + const untitledFileWorkingCopy = await manager.resolve(); - listener.dispose(); + assert.strictEqual(manager.workingCopies.length, 2); + assert.strictEqual(createCounter, 2); + assert.strictEqual(manager.get(fileWorkingCopy.resource), fileWorkingCopy); + assert.strictEqual(manager.get(untitledFileWorkingCopy.resource), untitledFileWorkingCopy); - workingCopy1.dispose(); - workingCopy2.dispose(); - workingCopy3.dispose(); + const sameFileWorkingCopy = await manager.resolve(fileUri); + const sameUntitledFileWorkingCopy = await manager.resolve({ untitledResource: untitledFileWorkingCopy.resource }); + assert.strictEqual(sameFileWorkingCopy, fileWorkingCopy); + assert.strictEqual(sameUntitledFileWorkingCopy, untitledFileWorkingCopy); + assert.strictEqual(manager.workingCopies.length, 2); + assert.strictEqual(createCounter, 2); + + fileWorkingCopy.dispose(); + untitledFileWorkingCopy.dispose(); }); - test('resolve async', async () => { - const resource = URI.file('/path/index.txt'); + test('resolve', async () => { + const fileWorkingCopy = await manager.resolve(URI.file('/test.html')); + assert.ok(fileWorkingCopy instanceof StoredFileWorkingCopy); + assert.strictEqual(await manager.stored.resolve(fileWorkingCopy.resource), fileWorkingCopy); - const workingCopy = await manager.resolve(resource); + const untitledFileWorkingCopy = await manager.resolve(); + assert.ok(untitledFileWorkingCopy instanceof UntitledFileWorkingCopy); + assert.strictEqual(await manager.untitled.resolve({ untitledResource: untitledFileWorkingCopy.resource }), untitledFileWorkingCopy); - let didResolve = false; - const onDidResolve = new Promise(resolve => { - manager.onDidResolve(() => { - if (workingCopy.resource.toString() === resource.toString()) { - didResolve = true; - resolve(); - } - }); - }); - - manager.resolve(resource, { reload: { async: true } }); - - await onDidResolve; - - assert.strictEqual(didResolve, true); + fileWorkingCopy.dispose(); + untitledFileWorkingCopy.dispose(); }); - test('resolve with initial contents', async () => { - const resource = URI.file('/test.html'); + test('destroy', async () => { + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); - const workingCopy = await manager.resolve(resource, { contents: bufferToStream(VSBuffer.fromString('Hello World')) }); - assert.strictEqual(workingCopy.model?.contents, 'Hello World'); - assert.strictEqual(workingCopy.isDirty(), true); + await manager.resolve(URI.file('/test.html')); + await manager.resolve({ contents: bufferToStream(VSBuffer.fromString('Hello Untitled')) }); - await manager.resolve(resource, { contents: bufferToStream(VSBuffer.fromString('More Changes')) }); - assert.strictEqual(workingCopy.model?.contents, 'More Changes'); - assert.strictEqual(workingCopy.isDirty(), true); + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 2); + assert.strictEqual(manager.stored.workingCopies.length, 1); + assert.strictEqual(manager.untitled.workingCopies.length, 1); - workingCopy.dispose(); + await manager.destroy(); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + assert.strictEqual(manager.stored.workingCopies.length, 0); + assert.strictEqual(manager.untitled.workingCopies.length, 0); }); - test('multiple resolves execute in sequence (same resources)', async () => { - const resource = URI.file('/test.html'); - - const firstPromise = manager.resolve(resource); - const secondPromise = manager.resolve(resource, { contents: bufferToStream(VSBuffer.fromString('Hello World')) }); - const thirdPromise = manager.resolve(resource, { contents: bufferToStream(VSBuffer.fromString('More Changes')) }); - - await firstPromise; - await secondPromise; - const workingCopy = await thirdPromise; - - assert.strictEqual(workingCopy.model?.contents, 'More Changes'); - assert.strictEqual(workingCopy.isDirty(), true); - - workingCopy.dispose(); - }); - - test('multiple resolves execute in parallel (different resources)', async () => { - const resource1 = URI.file('/test1.html'); - const resource2 = URI.file('/test2.html'); - const resource3 = URI.file('/test3.html'); - - const firstPromise = manager.resolve(resource1); - const secondPromise = manager.resolve(resource2); - const thirdPromise = manager.resolve(resource3); - - const [workingCopy1, workingCopy2, workingCopy3] = await Promise.all([firstPromise, secondPromise, thirdPromise]); - - assert.strictEqual(manager.workingCopies.length, 3); - assert.strictEqual(workingCopy1.resource.toString(), resource1.toString()); - assert.strictEqual(workingCopy2.resource.toString(), resource2.toString()); - assert.strictEqual(workingCopy3.resource.toString(), resource3.toString()); - - workingCopy1.dispose(); - workingCopy2.dispose(); - workingCopy3.dispose(); - }); - - test('removed from cache when working copy or model gets disposed', async () => { - const resource = URI.file('/test.html'); - - let workingCopy = await manager.resolve(resource, { contents: bufferToStream(VSBuffer.fromString('Hello World')) }); - - assert.strictEqual(manager.get(URI.file('/test.html')), workingCopy); - - workingCopy.dispose(); - assert(!manager.get(URI.file('/test.html'))); - - workingCopy = await manager.resolve(resource, { contents: bufferToStream(VSBuffer.fromString('Hello World')) }); - - assert.strictEqual(manager.get(URI.file('/test.html')), workingCopy); - - workingCopy.model?.dispose(); - assert(!manager.get(URI.file('/test.html'))); - }); - - test('events', async () => { - const resource1 = URI.file('/path/index.txt'); - const resource2 = URI.file('/path/other.txt'); - - let createdCounter = 0; - let resolvedCounter = 0; - let gotDirtyCounter = 0; - let gotNonDirtyCounter = 0; - let revertedCounter = 0; - let savedCounter = 0; - - manager.onDidCreate(workingCopy => { - createdCounter++; - }); - - manager.onDidResolve(workingCopy => { - if (workingCopy.resource.toString() === resource1.toString()) { - resolvedCounter++; - } - }); - - manager.onDidChangeDirty(workingCopy => { - if (workingCopy.resource.toString() === resource1.toString()) { - if (workingCopy.isDirty()) { - gotDirtyCounter++; - } else { - gotNonDirtyCounter++; - } - } - }); - - manager.onDidRevert(workingCopy => { - if (workingCopy.resource.toString() === resource1.toString()) { - revertedCounter++; - } - }); - - manager.onDidSave(({ workingCopy }) => { - if (workingCopy.resource.toString() === resource1.toString()) { - savedCounter++; - } - }); - - const workingCopy1 = await manager.resolve(resource1); - assert.strictEqual(resolvedCounter, 1); - assert.strictEqual(createdCounter, 1); - - accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.DELETED }], false)); - accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.ADDED }], false)); - - const workingCopy2 = await manager.resolve(resource2); - assert.strictEqual(resolvedCounter, 2); - assert.strictEqual(createdCounter, 2); - - workingCopy1.model?.updateContents('changed'); - - await workingCopy1.revert(); - workingCopy1.model?.updateContents('changed again'); - - await workingCopy1.save(); - workingCopy1.dispose(); - workingCopy2.dispose(); - - await workingCopy1.revert(); - assert.strictEqual(gotDirtyCounter, 2); - assert.strictEqual(gotNonDirtyCounter, 2); - assert.strictEqual(revertedCounter, 1); - assert.strictEqual(savedCounter, 1); - assert.strictEqual(createdCounter, 2); - - workingCopy1.dispose(); - workingCopy2.dispose(); - }); - - test('file change event triggers working copy resolve', async () => { - const resource = URI.file('/path/index.txt'); - - const workingCopy = await manager.resolve(resource); - - let didResolve = false; - const onDidResolve = new Promise(resolve => { - manager.onDidResolve(() => { - if (workingCopy.resource.toString() === resource.toString()) { - didResolve = true; - resolve(); - } - }); - }); - - accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.UPDATED }], false)); - - await onDidResolve; - - assert.strictEqual(didResolve, true); - }); - - test('working copy file event handling: create', async () => { - const resource = URI.file('/path/source.txt'); - - const workingCopy = await manager.resolve(resource); - workingCopy.model?.updateContents('hello create'); - assert.strictEqual(workingCopy.isDirty(), true); - - await accessor.workingCopyFileService.create([{ resource }], CancellationToken.None); - assert.strictEqual(workingCopy.isDirty(), false); - }); - - test('working copy file event handling: move', () => { - return testMoveCopyFileWorkingCopy(true); - }); - - test('working copy file event handling: copy', () => { - return testMoveCopyFileWorkingCopy(false); - }); - - async function testMoveCopyFileWorkingCopy(move: boolean) { - const source = URI.file('/path/source.txt'); - const target = URI.file('/path/other.txt'); - - const sourceWorkingCopy = await manager.resolve(source); - sourceWorkingCopy.model?.updateContents('hello move or copy'); - assert.strictEqual(sourceWorkingCopy.isDirty(), true); - - if (move) { - await accessor.workingCopyFileService.move([{ file: { source, target } }], CancellationToken.None); - } else { - await accessor.workingCopyFileService.copy([{ file: { source, target } }], CancellationToken.None); - } - - const targetWorkingCopy = await manager.resolve(target); - assert.strictEqual(targetWorkingCopy.isDirty(), true); - assert.strictEqual(targetWorkingCopy.model?.contents, 'hello move or copy'); - } - - test('working copy file event handling: delete', async () => { - const resource = URI.file('/path/source.txt'); - - const workingCopy = await manager.resolve(resource); - workingCopy.model?.updateContents('hello delete'); - assert.strictEqual(workingCopy.isDirty(), true); - - await accessor.workingCopyFileService.delete([{ resource }], CancellationToken.None); - assert.strictEqual(workingCopy.isDirty(), false); - }); - - test('working copy file event handling: move to same resource', async () => { + test('saveAs - file (same target, unresolved source, unresolved target)', () => { const source = URI.file('/path/source.txt'); - const sourceWorkingCopy = await manager.resolve(source); - sourceWorkingCopy.model?.updateContents('hello move'); - assert.strictEqual(sourceWorkingCopy.isDirty(), true); - - await accessor.workingCopyFileService.move([{ file: { source, target: source } }], CancellationToken.None); - - assert.strictEqual(sourceWorkingCopy.isDirty(), true); - assert.strictEqual(sourceWorkingCopy.model?.contents, 'hello move'); + return testSaveAsFile(source, source, false, false); }); - // saveAs: unresolved source, unresolved target - - test('saveAs (same target, unresolved source, unresolved target)', () => { - const source = URI.file('/path/source.txt'); - - return testSaveAs(source, source, false, false); - }); - - test('saveAs (same target, different case, unresolved source, unresolved target)', async () => { + test('saveAs - file (same target, different case, unresolved source, unresolved target)', async () => { const source = URI.file('/path/source.txt'); const target = URI.file('/path/SOURCE.txt'); - return testSaveAs(source, target, false, false); + return testSaveAsFile(source, target, false, false); }); - test('saveAs (different target, unresolved source, unresolved target)', async () => { + test('saveAs - file (different target, unresolved source, unresolved target)', async () => { const source = URI.file('/path/source.txt'); const target = URI.file('/path/target.txt'); - return testSaveAs(source, target, false, false); + return testSaveAsFile(source, target, false, false); }); - // saveAs: resolved source, unresolved target - - test('saveAs (same target, resolved source, unresolved target)', () => { + test('saveAs - file (same target, resolved source, unresolved target)', () => { const source = URI.file('/path/source.txt'); - return testSaveAs(source, source, true, false); + return testSaveAsFile(source, source, true, false); }); - test('saveAs (same target, different case, resolved source, unresolved target)', async () => { + test('saveAs - file (same target, different case, resolved source, unresolved target)', async () => { const source = URI.file('/path/source.txt'); const target = URI.file('/path/SOURCE.txt'); - return testSaveAs(source, target, true, false); + return testSaveAsFile(source, target, true, false); }); - test('saveAs (different target, resolved source, unresolved target)', async () => { + test('saveAs - file (different target, resolved source, unresolved target)', async () => { const source = URI.file('/path/source.txt'); const target = URI.file('/path/target.txt'); - return testSaveAs(source, target, true, false); + return testSaveAsFile(source, target, true, false); }); - // saveAs: unresolved source, resolved target - - test('saveAs (same target, unresolved source, resolved target)', () => { + test('saveAs - file (same target, unresolved source, resolved target)', () => { const source = URI.file('/path/source.txt'); - return testSaveAs(source, source, false, true); + return testSaveAsFile(source, source, false, true); }); - test('saveAs (same target, different case, unresolved source, resolved target)', async () => { + test('saveAs - file (same target, different case, unresolved source, resolved target)', async () => { const source = URI.file('/path/source.txt'); const target = URI.file('/path/SOURCE.txt'); - return testSaveAs(source, target, false, true); + return testSaveAsFile(source, target, false, true); }); - test('saveAs (different target, unresolved source, resolved target)', async () => { + test('saveAs - file (different target, unresolved source, resolved target)', async () => { const source = URI.file('/path/source.txt'); const target = URI.file('/path/target.txt'); - return testSaveAs(source, target, false, true); + return testSaveAsFile(source, target, false, true); }); - // saveAs: resolved source, resolved target - - test('saveAs (same target, resolved source, resolved target)', () => { + test('saveAs - file (same target, resolved source, resolved target)', () => { const source = URI.file('/path/source.txt'); - return testSaveAs(source, source, true, true); + return testSaveAsFile(source, source, true, true); }); - test('saveAs (different target, resolved source, resolved target)', async () => { + test('saveAs - file (different target, resolved source, resolved target)', async () => { const source = URI.file('/path/source.txt'); const target = URI.file('/path/target.txt'); - return testSaveAs(source, target, true, true); + return testSaveAsFile(source, target, true, true); }); - async function testSaveAs(source: URI, target: URI, resolveSource: boolean, resolveTarget: boolean) { - let sourceWorkingCopy: IFileWorkingCopy | undefined = undefined; + async function testSaveAsFile(source: URI, target: URI, resolveSource: boolean, resolveTarget: boolean) { + let sourceWorkingCopy: IStoredFileWorkingCopy | undefined = undefined; if (resolveSource) { sourceWorkingCopy = await manager.resolve(source); sourceWorkingCopy.model?.updateContents('hello world'); assert.ok(sourceWorkingCopy.isDirty()); } - let targetWorkingCopy: IFileWorkingCopy | undefined = undefined; + let targetWorkingCopy: IStoredFileWorkingCopy | undefined = undefined; if (resolveTarget) { targetWorkingCopy = await manager.resolve(target); targetWorkingCopy.model?.updateContents('hello world'); @@ -430,7 +201,15 @@ suite('FileWorkingCopyManager', () => { // the same in that case assert.strictEqual(source.toString(), result?.resource.toString()); } else { - assert.strictEqual(target.toString(), result?.resource.toString()); + if (resolveSource || resolveTarget) { + assert.strictEqual(target.toString(), result?.resource.toString()); + } else { + if (accessor.uriIdentityService.extUri.isEqual(source, target)) { + assert.strictEqual(undefined, result); + } else { + assert.strictEqual(target.toString(), result?.resource.toString()); + } + } } if (resolveSource) { @@ -442,58 +221,56 @@ suite('FileWorkingCopyManager', () => { } } - test('canDispose with dirty working copy', async () => { - const resource = URI.file('/path/index_something.txt'); + test('saveAs - untitled (without associated resource)', async () => { + const workingCopy = await manager.resolve(); + workingCopy.model?.updateContents('Simple Save As'); - const workingCopy = await manager.resolve(resource); - workingCopy.model?.updateContents('make dirty'); + const target = URI.file('simple/file.txt'); + accessor.fileDialogService.setPickFileToSave(target); - let canDisposePromise = manager.canDispose(workingCopy); - assert.ok(canDisposePromise instanceof Promise); + const result = await manager.saveAs(workingCopy.resource, undefined); + assert.strictEqual(result?.resource.toString(), target.toString()); - let canDispose = false; - (async () => { - canDispose = await canDisposePromise; - })(); + assert.strictEqual((result?.model as TestStoredFileWorkingCopyModel).contents, 'Simple Save As'); - assert.strictEqual(canDispose, false); - workingCopy.revert({ soft: true }); + assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); - await timeout(0); - - assert.strictEqual(canDispose, true); - - let canDispose2 = manager.canDispose(workingCopy); - assert.strictEqual(canDispose2, true); + workingCopy.dispose(); }); - test('pending saves join on shutdown', async () => { - const resource1 = URI.file('/path/index_something1.txt'); - const resource2 = URI.file('/path/index_something2.txt'); + test('saveAs - untitled (with associated resource)', async () => { + const workingCopy = await manager.resolve({ associatedResource: { path: '/some/associated.txt' } }); + workingCopy.model?.updateContents('Simple Save As with associated resource'); - const workingCopy1 = await manager.resolve(resource1); - workingCopy1.model?.updateContents('make dirty'); + const target = URI.from({ scheme: Schemas.vscodeRemote, path: '/some/associated.txt' }); - const workingCopy2 = await manager.resolve(resource2); - workingCopy2.model?.updateContents('make dirty'); + accessor.fileService.notExistsSet.set(target, true); - let saved1 = false; - workingCopy1.save().then(() => { - saved1 = true; - }); + const result = await manager.saveAs(workingCopy.resource, undefined); + assert.strictEqual(result?.resource.toString(), target.toString()); - let saved2 = false; - workingCopy2.save().then(() => { - saved2 = true; - }); + assert.strictEqual((result?.model as TestStoredFileWorkingCopyModel).contents, 'Simple Save As with associated resource'); - const event = new TestWillShutdownEvent(); - accessor.lifecycleService.fireWillShutdown(event); + assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); - assert.ok(event.value.length > 0); - await Promise.all(event.value); + workingCopy.dispose(); + }); - assert.strictEqual(saved1, true); - assert.strictEqual(saved2, true); + test('saveAs - untitled (target exists and is resolved)', async () => { + const workingCopy = await manager.resolve(); + workingCopy.model?.updateContents('Simple Save As'); + + const target = URI.file('simple/file.txt'); + const targetFileWorkingCopy = await manager.resolve(target); + accessor.fileDialogService.setPickFileToSave(target); + + const result = await manager.saveAs(workingCopy.resource, undefined); + assert.strictEqual(result, targetFileWorkingCopy); + + assert.strictEqual((result?.model as TestStoredFileWorkingCopyModel).contents, 'Simple Save As'); + + assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); + + workingCopy.dispose(); }); }); diff --git a/src/vs/workbench/services/workingCopy/test/browser/legacyBackupRestorer.test.ts b/src/vs/workbench/services/workingCopy/test/browser/legacyBackupRestorer.test.ts deleted file mode 100644 index 7456da9770..0000000000 --- a/src/vs/workbench/services/workingCopy/test/browser/legacyBackupRestorer.test.ts +++ /dev/null @@ -1,113 +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 { isWindows } from 'vs/base/common/platform'; -import { URI } from 'vs/base/common/uri'; -import { bufferToReadable, VSBuffer } from 'vs/base/common/buffer'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; -import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; -import { Schemas } from 'vs/base/common/network'; -import { isEqual } from 'vs/base/common/resources'; -import { createEditorPart, InMemoryTestWorkingCopyBackupService, registerTestResourceEditor, TestServiceAccessor, toUntypedWorkingCopyId, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { LegacyWorkingCopyBackupRestorer } from 'vs/workbench/services/workingCopy/common/legacyBackupRestorer'; -import { BrowserWorkingCopyBackupTracker } from 'vs/workbench/services/workingCopy/browser/workingCopyBackupTracker'; -import { DisposableStore } from 'vs/base/common/lifecycle'; - -suite('LegacyWorkingCopyBackupRestorer', () => { - - class TestBackupRestorer extends LegacyWorkingCopyBackupRestorer { - override async doRestoreLegacyBackups(): Promise { - return super.doRestoreLegacyBackups(); - } - } - - let accessor: TestServiceAccessor; - let disposables = new DisposableStore(); - - 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(() => { - disposables.add(registerTestResourceEditor()); - }); - - teardown(() => { - disposables.clear(); - }); - - test('Restore backups', async function () { - const workingCopyBackupService = new InMemoryTestWorkingCopyBackupService(); - const instantiationService = workbenchInstantiationService(); - instantiationService.stub(IWorkingCopyBackupService, workingCopyBackupService); - - const part = await createEditorPart(instantiationService, disposables); - - instantiationService.stub(IEditorGroupsService, part); - - const editorService: EditorService = instantiationService.createInstance(EditorService); - instantiationService.stub(IEditorService, editorService); - - accessor = instantiationService.createInstance(TestServiceAccessor); - - disposables.add(instantiationService.createInstance(BrowserWorkingCopyBackupTracker)); - const restorer = instantiationService.createInstance(TestBackupRestorer); - - // Backup 2 normal files and 2 untitled files - await workingCopyBackupService.backup(toUntypedWorkingCopyId(untitledFile1), bufferToReadable(VSBuffer.fromString('untitled-1'))); - await workingCopyBackupService.backup(toUntypedWorkingCopyId(untitledFile2), bufferToReadable(VSBuffer.fromString('untitled-2'))); - await workingCopyBackupService.backup(toUntypedWorkingCopyId(fooFile), bufferToReadable(VSBuffer.fromString('fooFile'))); - await workingCopyBackupService.backup(toUntypedWorkingCopyId(barFile), bufferToReadable(VSBuffer.fromString('barFile'))); - - // Verify backups restored and opened as dirty - await restorer.doRestoreLegacyBackups(); - assert.strictEqual(editorService.count, 4); - assert.ok(editorService.editors.every(editor => editor.isDirty())); - - let counter = 0; - for (const editor of editorService.editors) { - const resource = editor.resource; - if (isEqual(resource, untitledFile1)) { - const model = await accessor.textFileService.untitled.resolve({ untitledResource: resource }); - if (model.textEditorModel?.getValue() !== 'untitled-1') { - const backupContents = await workingCopyBackupService.getBackupContents(model); - assert.fail(`Unable to restore backup for resource ${untitledFile1.toString()}. Backup contents: ${backupContents}`); - } - model.dispose(); - counter++; - } else if (isEqual(resource, untitledFile2)) { - const model = await accessor.textFileService.untitled.resolve({ untitledResource: resource }); - if (model.textEditorModel?.getValue() !== 'untitled-2') { - const backupContents = await workingCopyBackupService.getBackupContents(model); - assert.fail(`Unable to restore backup for resource ${untitledFile2.toString()}. Backup contents: ${backupContents}`); - } - model.dispose(); - counter++; - } else if (isEqual(resource, fooFile)) { - const model = accessor.textFileService.files.get(fooFile); - await model?.resolve(); - if (model?.textEditorModel?.getValue() !== 'fooFile') { - const backupContents = await workingCopyBackupService.getBackupContents(model!); - assert.fail(`Unable to restore backup for resource ${fooFile.toString()}. Backup contents: ${backupContents}`); - } - counter++; - } else { - const model = accessor.textFileService.files.get(barFile); - await model?.resolve(); - if (model?.textEditorModel?.getValue() !== 'barFile') { - const backupContents = await workingCopyBackupService.getBackupContents(model!); - assert.fail(`Unable to restore backup for resource ${barFile.toString()}. Backup contents: ${backupContents}`); - } - counter++; - } - } - - assert.strictEqual(counter, 4); - }); -}); diff --git a/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopyTest.ts b/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopyTest.ts new file mode 100644 index 0000000000..e6f34fa941 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopyTest.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Event } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files'; +import { IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; +import { ResourceWorkingCopy } from 'vs/workbench/services/workingCopy/common/resourceWorkingCopy'; +import { WorkingCopyCapabilities, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopy'; + +suite('ResourceWorkingCopy', function () { + + class TestResourceWorkingCopy extends ResourceWorkingCopy { + name = 'testName'; + typeId = 'testTypeId'; + capabilities = WorkingCopyCapabilities.None; + onDidChangeDirty = Event.None; + onDidChangeContent = Event.None; + isDirty(): boolean { return false; } + async backup(token: CancellationToken): Promise { throw new Error('Method not implemented.'); } + async save(options?: ISaveOptions): Promise { return false; } + async revert(options?: IRevertOptions): Promise { } + + } + + let resource = URI.file('test/resource'); + let instantiationService: IInstantiationService; + let accessor: TestServiceAccessor; + let workingCopy: TestResourceWorkingCopy; + + function createWorkingCopy(uri: URI = resource) { + return new TestResourceWorkingCopy(uri, accessor.fileService); + } + + setup(() => { + instantiationService = workbenchInstantiationService(); + accessor = instantiationService.createInstance(TestServiceAccessor); + + workingCopy = createWorkingCopy(); + }); + + teardown(() => { + workingCopy.dispose(); + }); + + test('orphaned tracking', async () => { + assert.strictEqual(workingCopy.isOrphaned(), false); + + let onDidChangeOrphanedPromise = Event.toPromise(workingCopy.onDidChangeOrphaned); + accessor.fileService.notExistsSet.set(resource, true); + accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }], false)); + + await onDidChangeOrphanedPromise; + assert.strictEqual(workingCopy.isOrphaned(), true); + + onDidChangeOrphanedPromise = Event.toPromise(workingCopy.onDidChangeOrphaned); + accessor.fileService.notExistsSet.delete(resource); + accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.ADDED }], false)); + + await onDidChangeOrphanedPromise; + assert.strictEqual(workingCopy.isOrphaned(), false); + }); + + + test('dispose, isDisposed', async () => { + assert.strictEqual(workingCopy.isDisposed(), false); + + let disposedEvent = false; + workingCopy.onWillDispose(() => { + disposedEvent = true; + }); + + workingCopy.dispose(); + + assert.strictEqual(workingCopy.isDisposed(), true); + assert.strictEqual(disposedEvent, true); + }); +}); diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts similarity index 68% rename from src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts rename to src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts index 8ed08c10fe..cec9a0a13f 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts @@ -1,26 +1,26 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { FileWorkingCopy, FileWorkingCopyState, IFileWorkingCopyModel, IFileWorkingCopyModelContentChangedEvent, IFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { StoredFileWorkingCopy, StoredFileWorkingCopyState, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelContentChangedEvent, IStoredFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; import { bufferToStream, newWriteableBufferStream, streamToBuffer, VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable } from 'vs/base/common/lifecycle'; import { TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { basename } from 'vs/base/common/resources'; -import { FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; +import { FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult, NotModifiedSinceFileOperationError } from 'vs/platform/files/common/files'; import { SaveReason } from 'vs/workbench/common/editor'; import { Promises } from 'vs/base/common/async'; import { consumeReadable, consumeStream, isReadableStream } from 'vs/base/common/stream'; -export class TestFileWorkingCopyModel extends Disposable implements IFileWorkingCopyModel { +export class TestStoredFileWorkingCopyModel extends Disposable implements IStoredFileWorkingCopyModel { - private readonly _onDidChangeContent = this._register(new Emitter()); + private readonly _onDidChangeContent = this._register(new Emitter()); readonly onDidChangeContent = this._onDidChangeContent.event; private readonly _onWillDispose = this._register(new Emitter()); @@ -30,7 +30,7 @@ export class TestFileWorkingCopyModel extends Disposable implements IFileWorking super(); } - fireContentChangeEvent(event: IFileWorkingCopyModelContentChangedEvent): void { + fireContentChangeEvent(event: IStoredFileWorkingCopyModelContentChangedEvent): void { this._onDidChangeContent.fire(event); } @@ -38,7 +38,16 @@ export class TestFileWorkingCopyModel extends Disposable implements IFileWorking this.doUpdate(newContents); } + private throwOnSnapshot = false; + setThrowOnSnapshot(): void { + this.throwOnSnapshot = true; + } + async snapshot(token: CancellationToken): Promise { + if (this.throwOnSnapshot) { + throw new Error('Fail'); + } + const stream = newWriteableBufferStream(); stream.end(VSBuffer.fromString(this.contents)); @@ -72,24 +81,24 @@ export class TestFileWorkingCopyModel extends Disposable implements IFileWorking } } -export class TestFileWorkingCopyModelFactory implements IFileWorkingCopyModelFactory { +export class TestStoredFileWorkingCopyModelFactory implements IStoredFileWorkingCopyModelFactory { - async createModel(resource: URI, contents: VSBufferReadableStream, token: CancellationToken): Promise { - return new TestFileWorkingCopyModel(resource, (await streamToBuffer(contents)).toString()); + async createModel(resource: URI, contents: VSBufferReadableStream, token: CancellationToken): Promise { + return new TestStoredFileWorkingCopyModel(resource, (await streamToBuffer(contents)).toString()); } } -suite('FileWorkingCopy', function () { +suite('StoredFileWorkingCopy', function () { - const factory = new TestFileWorkingCopyModelFactory(); + const factory = new TestStoredFileWorkingCopyModelFactory(); let resource = URI.file('test/resource'); let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; - let workingCopy: FileWorkingCopy; + let workingCopy: StoredFileWorkingCopy; function createWorkingCopy(uri: URI = resource) { - return new FileWorkingCopy('testWorkingCopyType', uri, basename(uri), factory, accessor.fileService, accessor.logService, accessor.textFileService, accessor.filesConfigurationService, accessor.workingCopyBackupService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService); + return new StoredFileWorkingCopy('testStoredFileWorkingCopyType', uri, basename(uri), factory, accessor.fileService, accessor.logService, accessor.textFileService, accessor.filesConfigurationService, accessor.workingCopyBackupService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService); } setup(() => { @@ -103,31 +112,35 @@ suite('FileWorkingCopy', function () { workingCopy.dispose(); }); - test('requires good file system URI', async () => { - assert.throws(() => createWorkingCopy(URI.from({ scheme: 'unknown', path: 'somePath' }))); + test('registers with working copy service', async () => { + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 1); + + workingCopy.dispose(); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); }); test('orphaned tracking', async () => { - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN), false); let onDidChangeOrphanedPromise = Event.toPromise(workingCopy.onDidChangeOrphaned); - accessor.fileService.notExistsSet.add(resource); + accessor.fileService.notExistsSet.set(resource, true); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }], false)); await onDidChangeOrphanedPromise; - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN), true); onDidChangeOrphanedPromise = Event.toPromise(workingCopy.onDidChangeOrphaned); accessor.fileService.notExistsSet.delete(resource); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.ADDED }], false)); await onDidChangeOrphanedPromise; - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN), false); }); test('dirty', async () => { assert.strictEqual(workingCopy.isDirty(), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.DIRTY), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), false); await workingCopy.resolve(); assert.strictEqual(workingCopy.isResolved(), true); @@ -147,13 +160,13 @@ suite('FileWorkingCopy', function () { assert.strictEqual(contentChangeCounter, 1); assert.strictEqual(workingCopy.isDirty(), true); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.DIRTY), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), true); assert.strictEqual(changeDirtyCounter, 1); await workingCopy.save(); assert.strictEqual(workingCopy.isDirty(), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.DIRTY), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), false); assert.strictEqual(changeDirtyCounter, 2); // Dirty from: Initial contents @@ -161,26 +174,26 @@ suite('FileWorkingCopy', function () { assert.strictEqual(contentChangeCounter, 2); // content of model did not change assert.strictEqual(workingCopy.isDirty(), true); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.DIRTY), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), true); assert.strictEqual(changeDirtyCounter, 3); await workingCopy.revert({ soft: true }); assert.strictEqual(workingCopy.isDirty(), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.DIRTY), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), false); assert.strictEqual(changeDirtyCounter, 4); // Dirty from: API workingCopy.markDirty(); assert.strictEqual(workingCopy.isDirty(), true); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.DIRTY), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), true); assert.strictEqual(changeDirtyCounter, 5); await workingCopy.revert(); assert.strictEqual(workingCopy.isDirty(), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.DIRTY), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), false); assert.strictEqual(changeDirtyCounter, 6); }); @@ -256,6 +269,7 @@ suite('FileWorkingCopy', function () { await workingCopy.resolve(); assert.strictEqual(workingCopy.isDirty(), true); + assert.strictEqual(workingCopy.isReadonly(), false); assert.strictEqual(workingCopy.model?.contents, 'hello backup'); workingCopy.model.updateContents('hello updated'); @@ -273,11 +287,11 @@ suite('FileWorkingCopy', function () { const orphanedPromise = Event.toPromise(workingCopy.onDidChangeOrphaned); - accessor.fileService.notExistsSet.add(resource); + accessor.fileService.notExistsSet.set(resource, true); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }], false)); await orphanedPromise; - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN), true); const backup = await workingCopy.backup(CancellationToken.None); await accessor.workingCopyBackupService.backup(workingCopy, backup.content, undefined, backup.meta); @@ -289,7 +303,7 @@ suite('FileWorkingCopy', function () { workingCopy = createWorkingCopy(); await workingCopy.resolve(); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN), true); const backup2 = await workingCopy.backup(CancellationToken.None); assert.deepStrictEqual(backup.meta, backup2.meta); @@ -300,22 +314,22 @@ suite('FileWorkingCopy', function () { const orphanedPromise = Event.toPromise(workingCopy.onDidChangeOrphaned); - accessor.fileService.notExistsSet.add(resource); + accessor.fileService.notExistsSet.set(resource, true); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }], false)); await orphanedPromise; - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN), true); // resolving clears orphaned state when successful accessor.fileService.notExistsSet.delete(resource); await workingCopy.resolve({ forceReadFromFile: true }); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN), false); // resolving adds orphaned state when fail to read try { accessor.fileService.readShouldThrowError = new FileOperationError('file not found', FileOperationResult.FILE_NOT_FOUND); await workingCopy.resolve(); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN), true); } finally { accessor.fileService.readShouldThrowError = undefined; } @@ -334,6 +348,37 @@ suite('FileWorkingCopy', function () { assert.strictEqual(workingCopy.model?.contents, 'Hello Html'); }); + test('resolve (FILE_NOT_MODIFIED_SINCE still updates readonly state)', async () => { + let readonlyChangeCounter = 0; + workingCopy.onDidChangeReadonly(() => readonlyChangeCounter++); + + await workingCopy.resolve(); + + assert.strictEqual(workingCopy.isReadonly(), false); + + const stat = await accessor.fileService.resolve(workingCopy.resource, { resolveMetadata: true }); + + try { + accessor.fileService.readShouldThrowError = new NotModifiedSinceFileOperationError('file not modified since', { ...stat, readonly: true }); + await workingCopy.resolve(); + } finally { + accessor.fileService.readShouldThrowError = undefined; + } + + assert.strictEqual(workingCopy.isReadonly(), true); + assert.strictEqual(readonlyChangeCounter, 1); + + try { + accessor.fileService.readShouldThrowError = new NotModifiedSinceFileOperationError('file not modified since', { ...stat, readonly: false }); + await workingCopy.resolve(); + } finally { + accessor.fileService.readShouldThrowError = undefined; + } + + assert.strictEqual(workingCopy.isReadonly(), false); + assert.strictEqual(readonlyChangeCounter, 2); + }); + test('resolve does not alter content when model content changed in parallel', async () => { await workingCopy.resolve(); @@ -446,17 +491,17 @@ suite('FileWorkingCopy', function () { // save clears orphaned const orphanedPromise = Event.toPromise(workingCopy.onDidChangeOrphaned); - accessor.fileService.notExistsSet.add(resource); + accessor.fileService.notExistsSet.set(resource, true); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }], false)); await orphanedPromise; - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN), true); await workingCopy.save({ force: true }); assert.strictEqual(savedCounter, 6); assert.strictEqual(saveErrorCounter, 0); assert.strictEqual(workingCopy.isDirty(), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN), false); }); test('save (errors)', async () => { @@ -477,38 +522,36 @@ suite('FileWorkingCopy', function () { accessor.fileService.writeShouldThrowError = new FileOperationError('write error', FileOperationResult.FILE_PERMISSION_DENIED); await workingCopy.save({ force: true }); - } catch (error) { - // error is expected } finally { accessor.fileService.writeShouldThrowError = undefined; } assert.strictEqual(savedCounter, 0); assert.strictEqual(saveErrorCounter, 1); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ERROR), true); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.SAVED), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.PENDING_SAVE), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.CONFLICT), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ERROR), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.SAVED), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.PENDING_SAVE), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.CONFLICT), false); assert.strictEqual(workingCopy.isDirty(), true); // save is a no-op unless forced when in error case await workingCopy.save({ reason: SaveReason.AUTO }); assert.strictEqual(savedCounter, 0); assert.strictEqual(saveErrorCounter, 1); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ERROR), true); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.SAVED), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.PENDING_SAVE), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.CONFLICT), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ERROR), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.SAVED), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.PENDING_SAVE), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.CONFLICT), false); assert.strictEqual(workingCopy.isDirty(), true); // save clears error flags when successful await workingCopy.save({ reason: SaveReason.EXPLICIT }); assert.strictEqual(savedCounter, 1); assert.strictEqual(saveErrorCounter, 1); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ERROR), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.SAVED), true); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.PENDING_SAVE), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.CONFLICT), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ERROR), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.SAVED), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.PENDING_SAVE), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.CONFLICT), false); assert.strictEqual(workingCopy.isDirty(), false); // save error: conflict @@ -524,23 +567,40 @@ suite('FileWorkingCopy', function () { assert.strictEqual(savedCounter, 1); assert.strictEqual(saveErrorCounter, 2); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ERROR), true); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.SAVED), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.PENDING_SAVE), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.CONFLICT), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ERROR), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.SAVED), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.PENDING_SAVE), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.CONFLICT), true); assert.strictEqual(workingCopy.isDirty(), true); // save clears error flags when successful await workingCopy.save({ reason: SaveReason.EXPLICIT }); assert.strictEqual(savedCounter, 2); assert.strictEqual(saveErrorCounter, 2); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ERROR), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.SAVED), true); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.PENDING_SAVE), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.CONFLICT), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ERROR), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.SAVED), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.PENDING_SAVE), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.CONFLICT), false); assert.strictEqual(workingCopy.isDirty(), false); }); + test('save (errors, bubbles up with `ignoreErrorHandler`)', async () => { + await workingCopy.resolve(); + + let error: Error | undefined = undefined; + try { + accessor.fileService.writeShouldThrowError = new FileOperationError('write error', FileOperationResult.FILE_PERMISSION_DENIED); + + await workingCopy.save({ force: true, ignoreErrorHandler: true }); + } catch (e) { + error = e; + } finally { + accessor.fileService.writeShouldThrowError = undefined; + } + + assert.ok(error); + }); + test('revert', async () => { await workingCopy.resolve(); workingCopy.model?.updateContents('hello revert'); @@ -599,34 +659,34 @@ suite('FileWorkingCopy', function () { }); test('state', async () => { - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.SAVED), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.SAVED), true); await workingCopy.resolve({ contents: bufferToStream(VSBuffer.fromString('hello state')) }); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.DIRTY), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), true); const savePromise = workingCopy.save(); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.DIRTY), true); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.SAVED), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.PENDING_SAVE), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.SAVED), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.PENDING_SAVE), true); await savePromise; - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.DIRTY), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.SAVED), true); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.PENDING_SAVE), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.SAVED), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.PENDING_SAVE), false); }); test('joinState', async () => { await workingCopy.resolve({ contents: bufferToStream(VSBuffer.fromString('hello state')) }); workingCopy.save(); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.PENDING_SAVE), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.PENDING_SAVE), true); - await workingCopy.joinState(FileWorkingCopyState.PENDING_SAVE); + await workingCopy.joinState(StoredFileWorkingCopyState.PENDING_SAVE); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.DIRTY), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.SAVED), true); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.PENDING_SAVE), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.SAVED), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.PENDING_SAVE), false); }); test('isReadonly, isResolved, dispose, isDisposed', async () => { @@ -657,4 +717,24 @@ suite('FileWorkingCopy', function () { assert.strictEqual(disposedEvent, true); assert.strictEqual(disposedModelEvent, true); }); + + test('readonly change event', async () => { + accessor.fileService.readonly = true; + + await workingCopy.resolve(); + + assert.strictEqual(workingCopy.isReadonly(), true); + + accessor.fileService.readonly = false; + + let readonlyEvent = false; + workingCopy.onDidChangeReadonly(() => { + readonlyEvent = true; + }); + + await workingCopy.resolve(); + + assert.strictEqual(workingCopy.isReadonly(), false); + assert.strictEqual(readonlyEvent, true); + }); }); diff --git a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts new file mode 100644 index 0000000000..0ba9b3516d --- /dev/null +++ b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts @@ -0,0 +1,527 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { workbenchInstantiationService, TestServiceAccessor, TestWillShutdownEvent } from 'vs/workbench/test/browser/workbenchTestServices'; +import { StoredFileWorkingCopyManager, IStoredFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager'; +import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; +import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; +import { FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; +import { timeout } from 'vs/base/common/async'; +import { TestStoredFileWorkingCopyModel, TestStoredFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; + +suite('StoredFileWorkingCopyManager', () => { + + let instantiationService: IInstantiationService; + let accessor: TestServiceAccessor; + + let manager: IStoredFileWorkingCopyManager; + + setup(() => { + instantiationService = workbenchInstantiationService(); + accessor = instantiationService.createInstance(TestServiceAccessor); + + manager = new StoredFileWorkingCopyManager( + 'testStoredFileWorkingCopyType', + new TestStoredFileWorkingCopyModelFactory(), + accessor.fileService, accessor.lifecycleService, accessor.labelService, accessor.logService, + accessor.workingCopyFileService, accessor.workingCopyBackupService, accessor.uriIdentityService, + accessor.textFileService, accessor.filesConfigurationService, accessor.workingCopyService, accessor.notificationService, + accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService + ); + }); + + teardown(() => { + manager.dispose(); + }); + + test('resolve', async () => { + const resource = URI.file('/test.html'); + + const events: IStoredFileWorkingCopy[] = []; + const listener = manager.onDidCreate(workingCopy => { + events.push(workingCopy); + }); + + const resolvePromise = manager.resolve(resource); + assert.ok(manager.get(resource)); // working copy known even before resolved() + assert.strictEqual(manager.workingCopies.length, 1); + + const workingCopy1 = await resolvePromise; + assert.ok(workingCopy1); + assert.ok(workingCopy1.model); + assert.strictEqual(workingCopy1.typeId, 'testStoredFileWorkingCopyType'); + assert.strictEqual(workingCopy1.resource.toString(), resource.toString()); + assert.strictEqual(manager.get(resource), workingCopy1); + + const workingCopy2 = await manager.resolve(resource); + assert.strictEqual(workingCopy2, workingCopy1); + assert.strictEqual(manager.workingCopies.length, 1); + workingCopy1.dispose(); + + const workingCopy3 = await manager.resolve(resource); + assert.notStrictEqual(workingCopy3, workingCopy2); + assert.strictEqual(manager.workingCopies.length, 1); + assert.strictEqual(manager.get(resource), workingCopy3); + workingCopy3.dispose(); + + assert.strictEqual(manager.workingCopies.length, 0); + + assert.strictEqual(events.length, 2); + assert.strictEqual(events[0].resource.toString(), workingCopy1.resource.toString()); + assert.strictEqual(events[1].resource.toString(), workingCopy2.resource.toString()); + + listener.dispose(); + + workingCopy1.dispose(); + workingCopy2.dispose(); + workingCopy3.dispose(); + }); + + test('resolve async', async () => { + const resource = URI.file('/path/index.txt'); + + const workingCopy = await manager.resolve(resource); + + let didResolve = false; + const onDidResolve = new Promise(resolve => { + manager.onDidResolve(() => { + if (workingCopy.resource.toString() === resource.toString()) { + didResolve = true; + resolve(); + } + }); + }); + + manager.resolve(resource, { reload: { async: true } }); + + await onDidResolve; + + assert.strictEqual(didResolve, true); + }); + + test('resolve with initial contents', async () => { + const resource = URI.file('/test.html'); + + const workingCopy = await manager.resolve(resource, { contents: bufferToStream(VSBuffer.fromString('Hello World')) }); + assert.strictEqual(workingCopy.model?.contents, 'Hello World'); + assert.strictEqual(workingCopy.isDirty(), true); + + await manager.resolve(resource, { contents: bufferToStream(VSBuffer.fromString('More Changes')) }); + assert.strictEqual(workingCopy.model?.contents, 'More Changes'); + assert.strictEqual(workingCopy.isDirty(), true); + + workingCopy.dispose(); + }); + + test('multiple resolves execute in sequence (same resources)', async () => { + const resource = URI.file('/test.html'); + + const firstPromise = manager.resolve(resource); + const secondPromise = manager.resolve(resource, { contents: bufferToStream(VSBuffer.fromString('Hello World')) }); + const thirdPromise = manager.resolve(resource, { contents: bufferToStream(VSBuffer.fromString('More Changes')) }); + + await firstPromise; + await secondPromise; + const workingCopy = await thirdPromise; + + assert.strictEqual(workingCopy.model?.contents, 'More Changes'); + assert.strictEqual(workingCopy.isDirty(), true); + + workingCopy.dispose(); + }); + + test('multiple resolves execute in parallel (different resources)', async () => { + const resource1 = URI.file('/test1.html'); + const resource2 = URI.file('/test2.html'); + const resource3 = URI.file('/test3.html'); + + const firstPromise = manager.resolve(resource1); + const secondPromise = manager.resolve(resource2); + const thirdPromise = manager.resolve(resource3); + + const [workingCopy1, workingCopy2, workingCopy3] = await Promise.all([firstPromise, secondPromise, thirdPromise]); + + assert.strictEqual(manager.workingCopies.length, 3); + assert.strictEqual(workingCopy1.resource.toString(), resource1.toString()); + assert.strictEqual(workingCopy2.resource.toString(), resource2.toString()); + assert.strictEqual(workingCopy3.resource.toString(), resource3.toString()); + + workingCopy1.dispose(); + workingCopy2.dispose(); + workingCopy3.dispose(); + }); + + test('removed from cache when working copy or model gets disposed', async () => { + const resource = URI.file('/test.html'); + + let workingCopy = await manager.resolve(resource, { contents: bufferToStream(VSBuffer.fromString('Hello World')) }); + + assert.strictEqual(manager.get(URI.file('/test.html')), workingCopy); + + workingCopy.dispose(); + assert(!manager.get(URI.file('/test.html'))); + + workingCopy = await manager.resolve(resource, { contents: bufferToStream(VSBuffer.fromString('Hello World')) }); + + assert.strictEqual(manager.get(URI.file('/test.html')), workingCopy); + + workingCopy.model?.dispose(); + assert(!manager.get(URI.file('/test.html'))); + }); + + test('events', async () => { + const resource1 = URI.file('/path/index.txt'); + const resource2 = URI.file('/path/other.txt'); + + let createdCounter = 0; + let resolvedCounter = 0; + let gotDirtyCounter = 0; + let gotNonDirtyCounter = 0; + let revertedCounter = 0; + let savedCounter = 0; + let saveErrorCounter = 0; + + manager.onDidCreate(workingCopy => { + createdCounter++; + }); + + manager.onDidResolve(workingCopy => { + if (workingCopy.resource.toString() === resource1.toString()) { + resolvedCounter++; + } + }); + + manager.onDidChangeDirty(workingCopy => { + if (workingCopy.resource.toString() === resource1.toString()) { + if (workingCopy.isDirty()) { + gotDirtyCounter++; + } else { + gotNonDirtyCounter++; + } + } + }); + + manager.onDidRevert(workingCopy => { + if (workingCopy.resource.toString() === resource1.toString()) { + revertedCounter++; + } + }); + + manager.onDidSave(({ workingCopy }) => { + if (workingCopy.resource.toString() === resource1.toString()) { + savedCounter++; + } + }); + + manager.onDidSaveError(workingCopy => { + if (workingCopy.resource.toString() === resource1.toString()) { + saveErrorCounter++; + } + }); + + const workingCopy1 = await manager.resolve(resource1); + assert.strictEqual(resolvedCounter, 1); + assert.strictEqual(createdCounter, 1); + + accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.DELETED }], false)); + accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.ADDED }], false)); + + const workingCopy2 = await manager.resolve(resource2); + assert.strictEqual(resolvedCounter, 2); + assert.strictEqual(createdCounter, 2); + + workingCopy1.model?.updateContents('changed'); + + await workingCopy1.revert(); + workingCopy1.model?.updateContents('changed again'); + + await workingCopy1.save(); + + try { + accessor.fileService.writeShouldThrowError = new FileOperationError('write error', FileOperationResult.FILE_PERMISSION_DENIED); + + await workingCopy1.save({ force: true }); + } finally { + accessor.fileService.writeShouldThrowError = undefined; + } + + workingCopy1.dispose(); + workingCopy2.dispose(); + + await workingCopy1.revert(); + assert.strictEqual(gotDirtyCounter, 3); + assert.strictEqual(gotNonDirtyCounter, 2); + assert.strictEqual(revertedCounter, 1); + assert.strictEqual(savedCounter, 1); + assert.strictEqual(saveErrorCounter, 1); + assert.strictEqual(createdCounter, 2); + + workingCopy1.dispose(); + workingCopy2.dispose(); + }); + + test('resolve registers as working copy and dispose clears', async () => { + const resource1 = URI.file('/test1.html'); + const resource2 = URI.file('/test2.html'); + const resource3 = URI.file('/test3.html'); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + + const firstPromise = manager.resolve(resource1); + const secondPromise = manager.resolve(resource2); + const thirdPromise = manager.resolve(resource3); + + await Promise.all([firstPromise, secondPromise, thirdPromise]); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 3); + assert.strictEqual(manager.workingCopies.length, 3); + + manager.dispose(); + + assert.strictEqual(manager.workingCopies.length, 0); + + // dispose does not remove from working copy service, only `destroy` should + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 3); + }); + + test('destroy', async () => { + const resource1 = URI.file('/test1.html'); + const resource2 = URI.file('/test2.html'); + const resource3 = URI.file('/test3.html'); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + + const firstPromise = manager.resolve(resource1); + const secondPromise = manager.resolve(resource2); + const thirdPromise = manager.resolve(resource3); + + await Promise.all([firstPromise, secondPromise, thirdPromise]); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 3); + assert.strictEqual(manager.workingCopies.length, 3); + + await manager.destroy(); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + assert.strictEqual(manager.workingCopies.length, 0); + }); + + test('destroy saves dirty working copies', async () => { + const resource = URI.file('/path/source.txt'); + + const workingCopy = await manager.resolve(resource); + + let saved = false; + workingCopy.onDidSave(() => { + saved = true; + }); + + workingCopy.model?.updateContents('hello create'); + assert.strictEqual(workingCopy.isDirty(), true); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 1); + assert.strictEqual(manager.workingCopies.length, 1); + + await manager.destroy(); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + assert.strictEqual(manager.workingCopies.length, 0); + + assert.strictEqual(saved, true); + }); + + test('destroy falls back to using backup when save fails', async () => { + const resource = URI.file('/path/source.txt'); + + const workingCopy = await manager.resolve(resource); + workingCopy.model?.setThrowOnSnapshot(); + + let unexpectedSave = false; + workingCopy.onDidSave(() => { + unexpectedSave = true; + }); + + workingCopy.model?.updateContents('hello create'); + assert.strictEqual(workingCopy.isDirty(), true); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 1); + assert.strictEqual(manager.workingCopies.length, 1); + + assert.strictEqual(accessor.workingCopyBackupService.resolved.has(workingCopy), true); + + await manager.destroy(); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + assert.strictEqual(manager.workingCopies.length, 0); + + assert.strictEqual(unexpectedSave, false); + }); + + test('file change event triggers working copy resolve', async () => { + const resource = URI.file('/path/index.txt'); + + const workingCopy = await manager.resolve(resource); + + let didResolve = false; + const onDidResolve = new Promise(resolve => { + manager.onDidResolve(() => { + if (workingCopy.resource.toString() === resource.toString()) { + didResolve = true; + resolve(); + } + }); + }); + + accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.UPDATED }], false)); + + await onDidResolve; + + assert.strictEqual(didResolve, true); + }); + + test('file system provider change triggers working copy resolve', async () => { + const resource = URI.file('/path/index.txt'); + + const workingCopy = await manager.resolve(resource); + + let didResolve = false; + const onDidResolve = new Promise(resolve => { + manager.onDidResolve(() => { + if (workingCopy.resource.toString() === resource.toString()) { + didResolve = true; + resolve(); + } + }); + }); + + accessor.fileService.fireFileSystemProviderCapabilitiesChangeEvent({ provider: new InMemoryFileSystemProvider(), scheme: resource.scheme }); + + await onDidResolve; + + assert.strictEqual(didResolve, true); + }); + + test('working copy file event handling: create', async () => { + const resource = URI.file('/path/source.txt'); + + const workingCopy = await manager.resolve(resource); + workingCopy.model?.updateContents('hello create'); + assert.strictEqual(workingCopy.isDirty(), true); + + await accessor.workingCopyFileService.create([{ resource }], CancellationToken.None); + assert.strictEqual(workingCopy.isDirty(), false); + }); + + test('working copy file event handling: move', () => { + return testMoveCopyFileWorkingCopy(true); + }); + + test('working copy file event handling: copy', () => { + return testMoveCopyFileWorkingCopy(false); + }); + + async function testMoveCopyFileWorkingCopy(move: boolean) { + const source = URI.file('/path/source.txt'); + const target = URI.file('/path/other.txt'); + + const sourceWorkingCopy = await manager.resolve(source); + sourceWorkingCopy.model?.updateContents('hello move or copy'); + assert.strictEqual(sourceWorkingCopy.isDirty(), true); + + if (move) { + await accessor.workingCopyFileService.move([{ file: { source, target } }], CancellationToken.None); + } else { + await accessor.workingCopyFileService.copy([{ file: { source, target } }], CancellationToken.None); + } + + const targetWorkingCopy = await manager.resolve(target); + assert.strictEqual(targetWorkingCopy.isDirty(), true); + assert.strictEqual(targetWorkingCopy.model?.contents, 'hello move or copy'); + } + + test('working copy file event handling: delete', async () => { + const resource = URI.file('/path/source.txt'); + + const workingCopy = await manager.resolve(resource); + workingCopy.model?.updateContents('hello delete'); + assert.strictEqual(workingCopy.isDirty(), true); + + await accessor.workingCopyFileService.delete([{ resource }], CancellationToken.None); + assert.strictEqual(workingCopy.isDirty(), false); + }); + + test('working copy file event handling: move to same resource', async () => { + const source = URI.file('/path/source.txt'); + + const sourceWorkingCopy = await manager.resolve(source); + sourceWorkingCopy.model?.updateContents('hello move'); + assert.strictEqual(sourceWorkingCopy.isDirty(), true); + + await accessor.workingCopyFileService.move([{ file: { source, target: source } }], CancellationToken.None); + + assert.strictEqual(sourceWorkingCopy.isDirty(), true); + assert.strictEqual(sourceWorkingCopy.model?.contents, 'hello move'); + }); + + test('canDispose with dirty working copy', async () => { + const resource = URI.file('/path/index_something.txt'); + + const workingCopy = await manager.resolve(resource); + workingCopy.model?.updateContents('make dirty'); + + let canDisposePromise = manager.canDispose(workingCopy); + assert.ok(canDisposePromise instanceof Promise); + + let canDispose = false; + (async () => { + canDispose = await canDisposePromise; + })(); + + assert.strictEqual(canDispose, false); + workingCopy.revert({ soft: true }); + + await timeout(0); + + assert.strictEqual(canDispose, true); + + let canDispose2 = manager.canDispose(workingCopy); + assert.strictEqual(canDispose2, true); + }); + + test('pending saves join on shutdown', async () => { + const resource1 = URI.file('/path/index_something1.txt'); + const resource2 = URI.file('/path/index_something2.txt'); + + const workingCopy1 = await manager.resolve(resource1); + workingCopy1.model?.updateContents('make dirty'); + + const workingCopy2 = await manager.resolve(resource2); + workingCopy2.model?.updateContents('make dirty'); + + let saved1 = false; + workingCopy1.save().then(() => { + saved1 = true; + }); + + let saved2 = false; + workingCopy2.save().then(() => { + saved2 = true; + }); + + const event = new TestWillShutdownEvent(); + accessor.lifecycleService.fireWillShutdown(event); + + assert.ok(event.value.length > 0); + await Promise.all(event.value); + + assert.strictEqual(saved1, true); + assert.strictEqual(saved2, true); + }); +}); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts new file mode 100644 index 0000000000..fcf9344de8 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts @@ -0,0 +1,308 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { VSBufferReadableStream, newWriteableBufferStream, VSBuffer, streamToBuffer, bufferToStream } from 'vs/base/common/buffer'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { basename } from 'vs/base/common/resources'; +import { consumeReadable, consumeStream, isReadableStream } from 'vs/base/common/stream'; +import { URI } from 'vs/base/common/uri'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelContentChangedEvent, IUntitledFileWorkingCopyModelFactory, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; +import { TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; + +export class TestUntitledFileWorkingCopyModel extends Disposable implements IUntitledFileWorkingCopyModel { + + private readonly _onDidChangeContent = this._register(new Emitter()); + readonly onDidChangeContent = this._onDidChangeContent.event; + + private readonly _onWillDispose = this._register(new Emitter()); + readonly onWillDispose = this._onWillDispose.event; + + constructor(readonly resource: URI, public contents: string) { + super(); + } + + fireContentChangeEvent(event: IUntitledFileWorkingCopyModelContentChangedEvent): void { + this._onDidChangeContent.fire(event); + } + + updateContents(newContents: string): void { + this.doUpdate(newContents); + } + + private throwOnSnapshot = false; + setThrowOnSnapshot(): void { + this.throwOnSnapshot = true; + } + + async snapshot(token: CancellationToken): Promise { + if (this.throwOnSnapshot) { + throw new Error('Fail'); + } + + const stream = newWriteableBufferStream(); + stream.end(VSBuffer.fromString(this.contents)); + + return stream; + } + + async update(contents: VSBufferReadableStream, token: CancellationToken): Promise { + this.doUpdate((await streamToBuffer(contents)).toString()); + } + + private doUpdate(newContents: string): void { + this.contents = newContents; + + this.versionId++; + + this._onDidChangeContent.fire({ isEmpty: newContents.length === 0 }); + } + + versionId = 0; + + pushedStackElement = false; + + pushStackElement(): void { + this.pushedStackElement = true; + } + + override dispose(): void { + this._onWillDispose.fire(); + + super.dispose(); + } +} + +export class TestUntitledFileWorkingCopyModelFactory implements IUntitledFileWorkingCopyModelFactory { + + async createModel(resource: URI, contents: VSBufferReadableStream, token: CancellationToken): Promise { + return new TestUntitledFileWorkingCopyModel(resource, (await streamToBuffer(contents)).toString()); + } +} + +suite('UntitledFileWorkingCopy', () => { + + const factory = new TestUntitledFileWorkingCopyModelFactory(); + + let resource = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); + let instantiationService: IInstantiationService; + let accessor: TestServiceAccessor; + let workingCopy: UntitledFileWorkingCopy; + + function createWorkingCopy(uri: URI = resource, hasAssociatedFilePath = false, initialValue = '') { + return new UntitledFileWorkingCopy( + 'testUntitledWorkingCopyType', + uri, + basename(uri), + hasAssociatedFilePath, + initialValue.length > 0 ? bufferToStream(VSBuffer.fromString(initialValue)) : undefined, + factory, + async workingCopy => { await workingCopy.revert(); return true; }, + accessor.workingCopyService, + accessor.workingCopyBackupService, + accessor.logService + ); + } + + setup(() => { + instantiationService = workbenchInstantiationService(); + accessor = instantiationService.createInstance(TestServiceAccessor); + + workingCopy = createWorkingCopy(); + }); + + teardown(() => { + workingCopy.dispose(); + }); + + test('registers with working copy service', async () => { + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 1); + + workingCopy.dispose(); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + }); + + test('dirty', async () => { + assert.strictEqual(workingCopy.isDirty(), false); + + let changeDirtyCounter = 0; + workingCopy.onDidChangeDirty(() => { + changeDirtyCounter++; + }); + + let contentChangeCounter = 0; + workingCopy.onDidChangeContent(() => { + contentChangeCounter++; + }); + + await workingCopy.resolve(); + assert.strictEqual(workingCopy.isResolved(), true); + + // Dirty from: Model content change + workingCopy.model?.updateContents('hello dirty'); + assert.strictEqual(contentChangeCounter, 1); + + assert.strictEqual(workingCopy.isDirty(), true); + assert.strictEqual(changeDirtyCounter, 1); + + await workingCopy.save(); + + assert.strictEqual(workingCopy.isDirty(), false); + assert.strictEqual(changeDirtyCounter, 2); + }); + + test('dirty - cleared when content event signals isEmpty', async () => { + assert.strictEqual(workingCopy.isDirty(), false); + + await workingCopy.resolve(); + + workingCopy.model?.updateContents('hello dirty'); + assert.strictEqual(workingCopy.isDirty(), true); + + workingCopy.model?.fireContentChangeEvent({ isEmpty: true }); + + assert.strictEqual(workingCopy.isDirty(), false); + }); + + test('dirty - not cleared when content event signals isEmpty when associated resource', async () => { + workingCopy.dispose(); + workingCopy = createWorkingCopy(resource, true); + + await workingCopy.resolve(); + + workingCopy.model?.updateContents('hello dirty'); + assert.strictEqual(workingCopy.isDirty(), true); + + workingCopy.model?.fireContentChangeEvent({ isEmpty: true }); + + assert.strictEqual(workingCopy.isDirty(), true); + }); + + test('revert', async () => { + let revertCounter = 0; + workingCopy.onDidRevert(() => { + revertCounter++; + }); + + let disposeCounter = 0; + workingCopy.onWillDispose(() => { + disposeCounter++; + }); + + await workingCopy.resolve(); + + workingCopy.model?.updateContents('hello dirty'); + assert.strictEqual(workingCopy.isDirty(), true); + + await workingCopy.revert(); + + assert.strictEqual(revertCounter, 1); + assert.strictEqual(disposeCounter, 1); + assert.strictEqual(workingCopy.isDirty(), false); + }); + + test('dispose', async () => { + let disposeCounter = 0; + workingCopy.onWillDispose(() => { + disposeCounter++; + }); + + await workingCopy.resolve(); + workingCopy.dispose(); + + assert.strictEqual(disposeCounter, 1); + }); + + test('backup', async () => { + assert.strictEqual((await workingCopy.backup(CancellationToken.None)).content, undefined); + + await workingCopy.resolve(); + + workingCopy.model?.updateContents('Hello Backup'); + const backup = await workingCopy.backup(CancellationToken.None); + + let backupContents: string | undefined = undefined; + if (isReadableStream(backup.content)) { + backupContents = (await consumeStream(backup.content, chunks => VSBuffer.concat(chunks))).toString(); + } else if (backup.content) { + backupContents = consumeReadable(backup.content, chunks => VSBuffer.concat(chunks)).toString(); + } + + assert.strictEqual(backupContents, 'Hello Backup'); + }); + + test('resolve - without contents', async () => { + assert.strictEqual(workingCopy.isResolved(), false); + assert.strictEqual(workingCopy.hasAssociatedFilePath, false); + assert.strictEqual(workingCopy.model, undefined); + + await workingCopy.resolve(); + + assert.strictEqual(workingCopy.isResolved(), true); + assert.ok(workingCopy.model); + }); + + test('resolve - with initial contents', async () => { + workingCopy.dispose(); + + workingCopy = createWorkingCopy(resource, false, 'Hello Initial'); + + let contentChangeCounter = 0; + workingCopy.onDidChangeContent(() => { + contentChangeCounter++; + }); + + await workingCopy.resolve(); + + assert.strictEqual(workingCopy.isDirty(), true); + assert.strictEqual(workingCopy.model?.contents, 'Hello Initial'); + assert.strictEqual(contentChangeCounter, 1); + + workingCopy.model.updateContents('Changed contents'); + + await workingCopy.resolve(); // second resolve should be ignored + assert.strictEqual(workingCopy.model?.contents, 'Changed contents'); + }); + + test('resolve - with associated resource', async () => { + workingCopy.dispose(); + workingCopy = createWorkingCopy(resource, true); + + await workingCopy.resolve(); + + assert.strictEqual(workingCopy.isDirty(), true); + assert.strictEqual(workingCopy.hasAssociatedFilePath, true); + }); + + test('resolve - with backup', async () => { + await workingCopy.resolve(); + workingCopy.model?.updateContents('Hello Backup'); + + const backup = await workingCopy.backup(CancellationToken.None); + await accessor.workingCopyBackupService.backup(workingCopy, backup.content, undefined, backup.meta); + + assert.strictEqual(accessor.workingCopyBackupService.hasBackupSync(workingCopy), true); + + workingCopy.dispose(); + + workingCopy = createWorkingCopy(); + + let contentChangeCounter = 0; + workingCopy.onDidChangeContent(() => { + contentChangeCounter++; + }); + + await workingCopy.resolve(); + + assert.strictEqual(workingCopy.isDirty(), true); + assert.strictEqual(workingCopy.model?.contents, 'Hello Backup'); + assert.strictEqual(contentChangeCounter, 1); + }); +}); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts new file mode 100644 index 0000000000..102f2a1239 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts @@ -0,0 +1,238 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; +import { WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { TestStoredFileWorkingCopyModel, TestStoredFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test'; +import { TestUntitledFileWorkingCopyModel, TestUntitledFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test'; +import { TestInMemoryFileSystemProvider, TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; + +suite('UntitledFileWorkingCopyManager', () => { + + let instantiationService: IInstantiationService; + let accessor: TestServiceAccessor; + + let manager: IFileWorkingCopyManager; + + setup(() => { + instantiationService = workbenchInstantiationService(); + accessor = instantiationService.createInstance(TestServiceAccessor); + + accessor.fileService.registerProvider(Schemas.file, new TestInMemoryFileSystemProvider()); + accessor.fileService.registerProvider(Schemas.vscodeRemote, new TestInMemoryFileSystemProvider()); + + manager = new FileWorkingCopyManager( + 'testUntitledFileWorkingCopyType', + new TestStoredFileWorkingCopyModelFactory(), + new TestUntitledFileWorkingCopyModelFactory(), + accessor.fileService, accessor.lifecycleService, accessor.labelService, accessor.logService, + accessor.workingCopyFileService, accessor.workingCopyBackupService, accessor.uriIdentityService, accessor.fileDialogService, + accessor.textFileService, accessor.filesConfigurationService, accessor.workingCopyService, accessor.notificationService, + accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService, accessor.pathService, + accessor.environmentService, accessor.dialogService + ); + }); + + teardown(() => { + manager.dispose(); + }); + + test('basics', async () => { + let createCounter = 0; + manager.untitled.onDidCreate(e => { + createCounter++; + }); + + let disposeCounter = 0; + manager.untitled.onWillDispose(e => { + disposeCounter++; + }); + + let dirtyCounter = 0; + manager.untitled.onDidChangeDirty(e => { + dirtyCounter++; + }); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + assert.strictEqual(manager.untitled.workingCopies.length, 0); + + assert.strictEqual(manager.untitled.get(URI.file('/some/invalidPath')), undefined); + assert.strictEqual(manager.untitled.get(URI.file('/some/invalidPath').with({ scheme: Schemas.untitled })), undefined); + + const workingCopy1 = await manager.untitled.resolve(); + const workingCopy2 = await manager.untitled.resolve(); + + assert.strictEqual(workingCopy1.typeId, 'testUntitledFileWorkingCopyType'); + assert.strictEqual(workingCopy1.resource.scheme, Schemas.untitled); + + assert.strictEqual(createCounter, 2); + + assert.strictEqual(manager.untitled.get(workingCopy1.resource), workingCopy1); + assert.strictEqual(manager.untitled.get(workingCopy2.resource), workingCopy2); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 2); + assert.strictEqual(manager.untitled.workingCopies.length, 2); + + assert.notStrictEqual(workingCopy1.resource.toString(), workingCopy2.resource.toString()); + + for (const workingCopy of [workingCopy1, workingCopy2]) { + assert.strictEqual(workingCopy.capabilities, WorkingCopyCapabilities.Untitled); + assert.strictEqual(workingCopy.isDirty(), false); + assert.ok(workingCopy.model); + } + + workingCopy1.model?.updateContents('Hello World'); + + assert.strictEqual(workingCopy1.isDirty(), true); + assert.strictEqual(dirtyCounter, 1); + + workingCopy1.model?.updateContents(''); // change to empty clears dirty flag + assert.strictEqual(workingCopy1.isDirty(), false); + assert.strictEqual(dirtyCounter, 2); + + workingCopy2.model?.fireContentChangeEvent({ isEmpty: false }); + assert.strictEqual(workingCopy2.isDirty(), true); + assert.strictEqual(dirtyCounter, 3); + + workingCopy1.dispose(); + + assert.strictEqual(manager.untitled.workingCopies.length, 1); + assert.strictEqual(manager.untitled.get(workingCopy1.resource), undefined); + + workingCopy2.dispose(); + + assert.strictEqual(manager.untitled.workingCopies.length, 0); + assert.strictEqual(manager.untitled.get(workingCopy2.resource), undefined); + + assert.strictEqual(disposeCounter, 2); + }); + + test('resolve - with initial value', async () => { + let dirtyCounter = 0; + manager.untitled.onDidChangeDirty(e => { + dirtyCounter++; + }); + + const workingCopy = await manager.untitled.resolve({ contents: bufferToStream(VSBuffer.fromString('Hello World')) }); + + assert.strictEqual(workingCopy.isDirty(), true); + assert.strictEqual(dirtyCounter, 1); + assert.strictEqual(workingCopy.model?.contents, 'Hello World'); + + workingCopy.dispose(); + }); + + test('resolve - existing', async () => { + let createCounter = 0; + manager.untitled.onDidCreate(e => { + createCounter++; + }); + + const workingCopy1 = await manager.untitled.resolve(); + assert.strictEqual(createCounter, 1); + + const workingCopy2 = await manager.untitled.resolve({ untitledResource: workingCopy1.resource }); + assert.strictEqual(workingCopy1, workingCopy2); + assert.strictEqual(createCounter, 1); + + const workingCopy3 = await manager.untitled.resolve({ untitledResource: URI.file('/invalid/untitled') }); + assert.strictEqual(workingCopy3.resource.scheme, Schemas.untitled); + + workingCopy1.dispose(); + workingCopy2.dispose(); + workingCopy3.dispose(); + }); + + test('resolve - untitled resource used for new working copy', async () => { + const invalidUntitledResource = URI.file('my/untitled.txt'); + const validUntitledResource = invalidUntitledResource.with({ scheme: Schemas.untitled }); + + const workingCopy1 = await manager.untitled.resolve({ untitledResource: invalidUntitledResource }); + assert.notStrictEqual(workingCopy1.resource.toString(), invalidUntitledResource.toString()); + + const workingCopy2 = await manager.untitled.resolve({ untitledResource: validUntitledResource }); + assert.strictEqual(workingCopy2.resource.toString(), validUntitledResource.toString()); + + workingCopy1.dispose(); + workingCopy2.dispose(); + }); + + test('resolve - with associated resource', async () => { + const workingCopy = await manager.untitled.resolve({ associatedResource: { path: '/some/associated.txt' } }); + + assert.strictEqual(workingCopy.hasAssociatedFilePath, true); + assert.strictEqual(workingCopy.resource.path, '/some/associated.txt'); + + workingCopy.dispose(); + }); + + test('save - without associated resource', async () => { + const workingCopy = await manager.untitled.resolve(); + workingCopy.model?.updateContents('Simple Save'); + + accessor.fileDialogService.setPickFileToSave(URI.file('simple/file.txt')); + + const result = await workingCopy.save(); + assert.ok(result); + + assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); + + workingCopy.dispose(); + }); + + test('save - with associated resource', async () => { + const workingCopy = await manager.untitled.resolve({ associatedResource: { path: '/some/associated.txt' } }); + workingCopy.model?.updateContents('Simple Save with associated resource'); + + accessor.fileService.notExistsSet.set(URI.from({ scheme: Schemas.vscodeRemote, path: '/some/associated.txt' }), true); + + const result = await workingCopy.save(); + assert.ok(result); + + assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); + + workingCopy.dispose(); + }); + + test('save - with associated resource (asks to overwrite)', async () => { + const workingCopy = await manager.untitled.resolve({ associatedResource: { path: '/some/associated.txt' } }); + workingCopy.model?.updateContents('Simple Save with associated resource'); + + let result = await workingCopy.save(); + assert.ok(!result); // not confirmed + + assert.strictEqual(manager.untitled.get(workingCopy.resource), workingCopy); + + accessor.dialogService.setConfirmResult({ confirmed: true }); + + result = await workingCopy.save(); + assert.ok(result); // confirmed + + assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); + + workingCopy.dispose(); + }); + + test('destroy', async () => { + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + + await manager.untitled.resolve(); + await manager.untitled.resolve(); + await manager.untitled.resolve(); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 3); + assert.strictEqual(manager.untitled.workingCopies.length, 3); + + await manager.untitled.destroy(); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + assert.strictEqual(manager.untitled.workingCopies.length, 0); + }); +}); diff --git a/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts index fd32dca955..07184e8f7f 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts @@ -30,6 +30,8 @@ import { IWorkingCopyEditorHandler, IWorkingCopyEditorService } from 'vs/workben import { bufferToReadable, VSBuffer } from 'vs/base/common/buffer'; import { isWindows } from 'vs/base/common/platform'; import { Schemas } from 'vs/base/common/network'; +import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; +import { TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; suite('WorkingCopyBackupTracker (browser)', function () { let accessor: TestServiceAccessor; @@ -93,6 +95,7 @@ suite('WorkingCopyBackupTracker (browser)', function () { disposables.add(registerTestResourceEditor()); instantiationService.stub(IEditorGroupsService, part); + instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService(false)); const editorService: EditorService = instantiationService.createInstance(EditorService); instantiationService.stub(IEditorService, editorService); @@ -107,23 +110,23 @@ suite('WorkingCopyBackupTracker (browser)', function () { async function untitledBackupTest(untitled: IUntitledTextResourceEditorInput = {}): Promise { const { accessor, cleanup, workingCopyBackupService } = await createTracker(); - const untitledEditor = (await accessor.editorService.openEditor(untitled))?.input as UntitledTextEditorInput; + const untitledTextEditor = (await accessor.editorService.openEditor(untitled))?.input as UntitledTextEditorInput; - const untitledModel = await untitledEditor.resolve(); + const untitledTextModel = await untitledTextEditor.resolve(); if (!untitled?.contents) { - untitledModel.textEditorModel?.setValue('Super Good'); + untitledTextModel.textEditorModel?.setValue('Super Good'); } await workingCopyBackupService.joinBackupResource(); - assert.strictEqual(workingCopyBackupService.hasBackupSync(untitledModel), true); + assert.strictEqual(workingCopyBackupService.hasBackupSync(untitledTextModel), true); - untitledModel.dispose(); + untitledTextModel.dispose(); await workingCopyBackupService.joinDiscardBackup(); - assert.strictEqual(workingCopyBackupService.hasBackupSync(untitledModel), false); + assert.strictEqual(workingCopyBackupService.hasBackupSync(untitledTextModel), false); cleanup(); } @@ -199,6 +202,7 @@ suite('WorkingCopyBackupTracker (browser)', function () { const part = await createEditorPart(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, part); + instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService(false)); const editorService: EditorService = instantiationService.createInstance(EditorService); instantiationService.stub(IEditorService, editorService); diff --git a/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts index 80c239947d..a5af0223c0 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts @@ -1,15 +1,17 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; +import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { IWorkingCopyEditorHandler, WorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; +import { TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; import { createEditorPart, registerTestResourceEditor, TestEditorService, TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; @@ -52,6 +54,7 @@ suite('WorkingCopyEditorService', () => { const instantiationService = workbenchInstantiationService(); const part = await createEditorPart(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, part); + instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService(false)); const editorService = instantiationService.createInstance(EditorService); const accessor = instantiationService.createInstance(TestServiceAccessor); diff --git a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts index 585460f9a4..ea7eb10299 100644 --- a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts @@ -33,6 +33,9 @@ suite('WorkingCopyService', () => { // resource 1 const resource1 = URI.file('/some/folder/file.txt'); + assert.strictEqual(service.has(resource1), false); + assert.strictEqual(service.has({ resource: resource1, typeId: 'testWorkingCopyType' }), false); + assert.strictEqual(service.get({ resource: resource1, typeId: 'testWorkingCopyType' }), undefined); const copy1 = new TestWorkingCopy(resource1); const unregister1 = service.registerWorkingCopy(copy1); @@ -42,6 +45,9 @@ suite('WorkingCopyService', () => { assert.strictEqual(onDidRegister[0], copy1); assert.strictEqual(service.dirtyCount, 0); assert.strictEqual(service.isDirty(resource1), false); + assert.strictEqual(service.has(resource1), true); + assert.strictEqual(service.has(copy1), true); + assert.strictEqual(service.get(copy1), copy1); assert.strictEqual(service.hasDirty, false); copy1.setDirty(true); @@ -75,6 +81,7 @@ suite('WorkingCopyService', () => { assert.strictEqual(onDidUnregister.length, 1); assert.strictEqual(onDidUnregister[0], copy1); assert.strictEqual(service.workingCopies.length, 0); + assert.strictEqual(service.has(resource1), false); // resource 2 const resource2 = URI.file('/some/folder/file-dirty.txt'); diff --git a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts index 93f509932a..c8617b3b1c 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; @@ -10,9 +10,9 @@ import { createHash } from 'crypto'; import { insert } from 'vs/base/common/arrays'; import { hash } from 'vs/base/common/hash'; import { isEqual } from 'vs/base/common/resources'; -import { promises, existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; +import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; import { dirname, join } from 'vs/base/common/path'; -import { readdirSync, rimraf, writeFile } from 'vs/base/node/pfs'; +import { Promises, readdirSync } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { WorkingCopyBackupsModel, hashIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopyBackupService'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; @@ -47,6 +47,7 @@ export class NodeTestWorkingCopyBackupService extends NativeWorkingCopyBackupSer private backupResourceJoiners: Function[]; private discardBackupJoiners: Function[]; discardedBackups: IWorkingCopyIdentifier[]; + discardedAllBackups: boolean; private pendingBackupsArr: Promise[]; private diskFileSystemProvider: DiskFileSystemProvider; @@ -65,6 +66,7 @@ export class NodeTestWorkingCopyBackupService extends NativeWorkingCopyBackupSer this.discardBackupJoiners = []; this.discardedBackups = []; this.pendingBackupsArr = []; + this.discardedAllBackups = false; } async waitForAllBackups(): Promise { @@ -103,6 +105,12 @@ export class NodeTestWorkingCopyBackupService extends NativeWorkingCopyBackupSer } } + override async discardBackups(filter?: { except: IWorkingCopyIdentifier[] }): Promise { + this.discardedAllBackups = true; + + return super.discardBackups(filter); + } + async getBackupContents(identifier: IWorkingCopyIdentifier): Promise { const backupResource = this.toBackupResource(identifier); @@ -141,14 +149,14 @@ suite('WorkingCopyBackupService', () => { service = new NodeTestWorkingCopyBackupService(testDir, workspaceBackupPath); - await promises.mkdir(backupHome, { recursive: true }); + await Promises.mkdir(backupHome, { recursive: true }); - return writeFile(workspacesJsonPath, ''); + return Promises.writeFile(workspacesJsonPath, ''); }); teardown(() => { service.dispose(); - return rimraf(testDir); + return Promises.rm(testDir); }); suite('hashIdentifier', () => { @@ -610,7 +618,7 @@ suite('WorkingCopyBackupService', () => { await service.backup(backupId3, bufferToReadable(VSBuffer.fromString('test'))); assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 3); - await service.discardBackups([backupId2, backupId3]); + await service.discardBackups({ except: [backupId2, backupId3] }); let backupPath = join(workspaceBackupPath, backupId1.resource.scheme, hashIdentifier(backupId1)); assert.strictEqual(existsSync(backupPath), false); @@ -621,7 +629,7 @@ suite('WorkingCopyBackupService', () => { backupPath = join(workspaceBackupPath, backupId3.resource.scheme, hashIdentifier(backupId3)); assert.strictEqual(existsSync(backupPath), true); - await service.discardBackups([backupId1]); + await service.discardBackups({ except: [backupId1] }); for (const backupId of [backupId1, backupId2, backupId3]) { const backupPath = join(workspaceBackupPath, backupId.resource.scheme, hashIdentifier(backupId)); @@ -637,7 +645,7 @@ suite('WorkingCopyBackupService', () => { assert.strictEqual(existsSync(backupPath), true); assert.strictEqual(readdirSync(join(workspaceBackupPath, 'untitled')).length, 1); - await service.discardBackups([backupId]); + await service.discardBackups({ except: [backupId] }); assert.strictEqual(existsSync(backupPath), true); }); }); @@ -982,7 +990,7 @@ suite('WorkingCopyBackupService', () => { const sourceDir = getPathFromAmdModule(require, './fixtures'); - const buffer = await promises.readFile(join(sourceDir, 'binary.txt')); + const buffer = await Promises.readFile(join(sourceDir, 'binary.txt')); const hash = createHash('md5').update(buffer).digest('base64'); await service.backup(identifier, bufferToReadable(VSBuffer.wrap(buffer)), undefined, { binaryTest: 'true' }); @@ -1059,7 +1067,7 @@ suite('WorkingCopyBackupService', () => { test('create', async () => { const fooBackupPath = join(workspaceBackupPath, fooFile.scheme, hashIdentifier(toUntypedWorkingCopyId(fooFile))); - await promises.mkdir(dirname(fooBackupPath), { recursive: true }); + await Promises.mkdir(dirname(fooBackupPath), { recursive: true }); writeFileSync(fooBackupPath, 'foo'); const model = await WorkingCopyBackupsModel.create(URI.file(workspaceBackupPath), service.fileService); diff --git a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupTracker.test.ts b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupTracker.test.ts index 5715075168..6c3fb9a4b2 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupTracker.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupTracker.test.ts @@ -6,9 +6,8 @@ import * as assert from 'assert'; 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 { Promises } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; import { hash } from 'vs/base/common/hash'; @@ -30,7 +29,6 @@ import { ShutdownReason, ILifecycleService } from 'vs/workbench/services/lifecyc import { IFileDialogService, ConfirmResult, IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; -import { WorkingCopyBackupTracker } from 'vs/workbench/services/workingCopy/common/workingCopyBackupTracker'; import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; @@ -48,7 +46,7 @@ import { IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/wor flakySuite('WorkingCopyBackupTracker (native)', function () { - class TestBackupTracker extends NativeWorkingCopyBackupTracker { + class TestWorkingCopyBackupTracker extends NativeWorkingCopyBackupTracker { constructor( @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, @@ -63,16 +61,19 @@ flakySuite('WorkingCopyBackupTracker (native)', function () { @IEditorService editorService: IEditorService, @IEnvironmentService environmentService: IEnvironmentService, @IProgressService progressService: IProgressService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, @IWorkingCopyEditorService workingCopyEditorService: IWorkingCopyEditorService ) { - super(workingCopyBackupService, filesConfigurationService, workingCopyService, lifecycleService, fileDialogService, dialogService, contextService, nativeHostService, logService, environmentService, progressService, editorGroupService, workingCopyEditorService, editorService); + super(workingCopyBackupService, filesConfigurationService, workingCopyService, lifecycleService, fileDialogService, dialogService, contextService, nativeHostService, logService, environmentService, progressService, workingCopyEditorService, editorService); } protected override getBackupScheduleDelay(): number { return 10; // Reduce timeout for tests } + waitForReady(): Promise { + return super.whenReady; + } + override dispose() { super.dispose(); @@ -89,8 +90,6 @@ flakySuite('WorkingCopyBackupTracker (native)', function () { let accessor: TestServiceAccessor; const disposables = new DisposableStore(); - this.retries(3); - this.timeout(1000 * 20); setup(async () => { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'backuprestorer'); backupHome = join(testDir, 'Backups'); @@ -105,19 +104,19 @@ flakySuite('WorkingCopyBackupTracker (native)', function () { disposables.add(registerTestFileEditor()); - await promises.mkdir(backupHome, { recursive: true }); - await promises.mkdir(workspaceBackupPath, { recursive: true }); + await Promises.mkdir(backupHome, { recursive: true }); + await Promises.mkdir(workspaceBackupPath, { recursive: true }); - return writeFile(workspacesJsonPath, ''); + return Promises.writeFile(workspacesJsonPath, ''); }); teardown(async () => { disposables.clear(); - return rimraf(testDir); + return Promises.rm(testDir); }); - async function createTracker(autoSaveEnabled = false): Promise<{ accessor: TestServiceAccessor, part: EditorPart, tracker: WorkingCopyBackupTracker, instantiationService: IInstantiationService, cleanup: () => Promise }> { + async function createTracker(autoSaveEnabled = false): Promise<{ accessor: TestServiceAccessor, part: EditorPart, tracker: TestWorkingCopyBackupTracker, instantiationService: IInstantiationService, cleanup: () => Promise }> { const workingCopyBackupService = new NodeTestWorkingCopyBackupService(testDir, workspaceBackupPath); const instantiationService = workbenchInstantiationService(); instantiationService.stub(IWorkingCopyBackupService, workingCopyBackupService); @@ -142,7 +141,7 @@ flakySuite('WorkingCopyBackupTracker (native)', function () { accessor = instantiationService.createInstance(TestServiceAccessor); - const tracker = instantiationService.createInstance(TestBackupTracker); + const tracker = instantiationService.createInstance(TestWorkingCopyBackupTracker); const cleanup = async () => { // File changes could also schedule some backup operations so we need to wait for them before finishing the test @@ -271,6 +270,34 @@ flakySuite('WorkingCopyBackupTracker (native)', function () { await cleanup(); }); + test('onWillShutdown - no backups discarded when shutdown without dirty but tracker not ready', async function () { + const { accessor, cleanup } = await createTracker(); + + const event = new TestBeforeShutdownEvent(); + accessor.lifecycleService.fireBeforeShutdown(event); + + const veto = await event.value; + assert.ok(!veto); + assert.ok(!accessor.workingCopyBackupService.discardedAllBackups); + + await cleanup(); + }); + + test('onWillShutdown - backups discarded when shutdown without dirty', async function () { + const { accessor, tracker, cleanup } = await createTracker(); + + await tracker.waitForReady(); + + const event = new TestBeforeShutdownEvent(); + accessor.lifecycleService.fireBeforeShutdown(event); + + const veto = await event.value; + assert.ok(!veto); + assert.ok(!accessor.workingCopyBackupService.discardedAllBackups); + + await cleanup(); + }); + test('onWillShutdown - save (hot.exit: off)', async function () { const { accessor, cleanup } = await createTracker(); diff --git a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts index 23145548df..f56e08ab6b 100644 --- a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts @@ -14,7 +14,7 @@ import { ConfigurationScope, IConfigurationRegistry, Extensions as Configuration import { Registry } from 'vs/platform/registry/common/platform'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { distinct } from 'vs/base/common/arrays'; -import { dirname, isEqual, isEqualAuthority } from 'vs/base/common/resources'; +import { isEqual, isEqualAuthority } from 'vs/base/common/resources'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IFileService } from 'vs/platform/files/common/files'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -138,7 +138,7 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi const remoteAuthority = this.environmentService.remoteAuthority; if (remoteAuthority) { // https://github.com/microsoft/vscode/issues/94191 - foldersToAdd = foldersToAdd.filter(f => f.uri.scheme !== Schemas.file && (f.uri.scheme !== Schemas.vscodeRemote || isEqualAuthority(f.uri.authority, remoteAuthority))); + foldersToAdd = foldersToAdd.filter(folder => folder.uri.scheme !== Schemas.file && (folder.uri.scheme !== Schemas.vscodeRemote || isEqualAuthority(folder.uri.authority, remoteAuthority))); } // If we are in no-workspace or single-folder workspace, adding folders has to @@ -258,7 +258,7 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi await this.textFileService.create([{ resource: targetConfigPathURI, value: newRawWorkspaceContents, options: { overwrite: true } }]); // Set trust for the workspace file - this.trustWorkspaceConfiguration(targetConfigPathURI); + await this.trustWorkspaceConfiguration(targetConfigPathURI); } protected async saveWorkspace(workspace: IWorkspaceIdentifier): Promise { @@ -317,7 +317,7 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi abstract enterWorkspace(path: URI): Promise; - protected async doEnterWorkspace(path: URI): Promise { + protected async doEnterWorkspace(path: URI): Promise { if (!!this.environmentService.extensionTestsLocationURI) { throw new Error('Entering a new workspace is not possible in tests.'); } @@ -359,9 +359,9 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi return this.jsonEditingService.write(toWorkspace.configPath, [{ path: ['settings'], value: targetWorkspaceConfiguration }], true); } - private trustWorkspaceConfiguration(configPathURI: URI): void { + private async trustWorkspaceConfiguration(configPathURI: URI): Promise { if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.workspaceTrustManagementService.isWorkpaceTrusted()) { - this.workspaceTrustManagementService.setFoldersTrust([dirname(configPathURI)], true); + await this.workspaceTrustManagementService.setUrisTrust([configPathURI], true); } } diff --git a/src/vs/workbench/services/workspaces/browser/workspaceTrustEditorInput.ts b/src/vs/workbench/services/workspaces/browser/workspaceTrustEditorInput.ts index b24e0bb70c..fcb8d04c27 100644 --- a/src/vs/workbench/services/workspaces/browser/workspaceTrustEditorInput.ts +++ b/src/vs/workbench/services/workspaces/browser/workspaceTrustEditorInput.ts @@ -1,12 +1,12 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { EditorInput } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; export class WorkspaceTrustEditorInput extends EditorInput { static readonly ID: string = 'workbench.input.workspaceTrust'; diff --git a/src/vs/workbench/services/workspaces/browser/workspacesService.ts b/src/vs/workbench/services/workspaces/browser/workspacesService.ts index 519a521364..73158b8a10 100644 --- a/src/vs/workbench/services/workspaces/browser/workspacesService.ts +++ b/src/vs/workbench/services/workspaces/browser/workspacesService.ts @@ -125,7 +125,7 @@ export class BrowserWorkspacesService extends Disposable implements IWorkspacesS //#region Workspace Management - async enterWorkspace(path: URI): Promise { + async enterWorkspace(path: URI): Promise { return { workspace: await this.getWorkspaceIdentifier(path) }; } diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index ac0463e559..c284df93b1 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -1,42 +1,70 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Codicon } from 'vs/base/common/codicons'; import { Emitter } from 'vs/base/common/event'; +import { MarkdownString } from 'vs/base/common/htmlContent'; import { splitName } from 'vs/base/common/labels'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { LinkedList } from 'vs/base/common/linkedList'; import { Schemas } from 'vs/base/common/network'; -import { isWeb } from 'vs/base/common/platform'; -import { dirname } from 'vs/base/common/resources'; +import Severity from 'vs/base/common/severity'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IRemoteAuthorityResolverService, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { isVirtualResource } from 'vs/platform/remote/common/remoteHosts'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { WorkspaceTrustRequestOptions, IWorkspaceTrustManagementService, IWorkspaceTrustInfo, IWorkspaceTrustUriInfo, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; +import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { WorkspaceTrustRequestOptions, IWorkspaceTrustManagementService, IWorkspaceTrustInfo, IWorkspaceTrustUriInfo, IWorkspaceTrustRequestService, IWorkspaceTrustTransitionParticipant, WorkspaceTrustUriResponse } from 'vs/platform/workspace/common/workspaceTrust'; import { isSingleFolderWorkspaceIdentifier, isUntitledWorkspace, toWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { Memento, MementoObject } from 'vs/workbench/common/memento'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; export const WORKSPACE_TRUST_ENABLED = 'security.workspace.trust.enabled'; export const WORKSPACE_TRUST_STARTUP_PROMPT = 'security.workspace.trust.startupPrompt'; +export const WORKSPACE_TRUST_UNTRUSTED_FILES = 'security.workspace.trust.untrustedFiles'; +export const WORKSPACE_TRUST_EMPTY_WINDOW = 'security.workspace.trust.emptyWindow'; export const WORKSPACE_TRUST_EXTENSION_SUPPORT = 'extensions.supportUntrustedWorkspaces'; export const WORKSPACE_TRUST_STORAGE_KEY = 'content.trust.model.key'; export const WorkspaceTrustContext = { - PendingRequest: new RawContextKey('workspaceTrustPendingRequest', false), - IsTrusted: new RawContextKey('isWorkspaceTrusted', false, localize('workspaceTrustCtx', "Whether the current workspace has been trusted by the user.")) + IsEnabled: new RawContextKey('isWorkspaceTrustEnabled', false, localize('workspaceTrustEnabledCtx', "Whether the workspace trust feature is enabled.")), + IsTrusted: new RawContextKey('isWorkspaceTrusted', false, localize('workspaceTrustedCtx', "Whether the current workspace has been trusted by the user.")) }; -export function isWorkspaceTrustEnabled(configurationService: IConfigurationService): boolean { - if (isWeb) { - return false; +export class CanonicalWorkspace implements IWorkspace { + constructor( + private readonly originalWorkspace: IWorkspace, + private readonly canonicalFolderUris: URI[], + private readonly canonicalConfiguration: URI | null | undefined + ) { } + + + get folders(): IWorkspaceFolder[] { + return this.originalWorkspace.folders.map((folder, index) => { + return { + index: folder.index, + name: folder.name, + toResource: folder.toResource, + uri: this.canonicalFolderUris[index] + }; + }); } - return configurationService.inspect(WORKSPACE_TRUST_ENABLED).userValue ?? false; + get configuration(): URI | null | undefined { + return this.canonicalConfiguration ?? this.originalWorkspace.configuration; + } + + get id(): string { + return this.originalWorkspace.id; + } } export class WorkspaceTrustManagementService extends Disposable implements IWorkspaceTrustManagementService { @@ -45,50 +73,120 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork private readonly storageKey = WORKSPACE_TRUST_STORAGE_KEY; + private _initialized: boolean; + private _workspaceResolvedPromise: Promise; + private _workspaceResolvedPromiseResolve!: () => void; + private _workspaceTrustInitializedPromise: Promise; + private _workspaceTrustInitializedPromiseResolve!: () => void; + private _remoteAuthority: ResolverResult | undefined; + private readonly _onDidChangeTrust = this._register(new Emitter()); readonly onDidChangeTrust = this._onDidChangeTrust.event; private readonly _onDidChangeTrustedFolders = this._register(new Emitter()); readonly onDidChangeTrustedFolders = this._onDidChangeTrustedFolders.event; - private _isWorkspaceTrusted: boolean = false; private _trustStateInfo: IWorkspaceTrustInfo; + private _canonicalWorkspace: IWorkspace; + + protected readonly _trustState: WorkspaceTrustState; + private readonly _trustTransitionManager: WorkspaceTrustTransitionManager; constructor( - @IConfigurationService readonly configurationService: IConfigurationService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IConfigurationService protected readonly configurationService: IConfigurationService, @IStorageService private readonly storageService: IStorageService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, + @IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService, + ) { super(); + this._canonicalWorkspace = this.workspaceService.getWorkspace(); + this._initialized = false; + this._workspaceResolvedPromise = new Promise((resolve) => { + this._workspaceResolvedPromiseResolve = resolve; + }); + this._workspaceTrustInitializedPromise = new Promise((resolve) => { + this._workspaceTrustInitializedPromiseResolve = resolve; + }); + + this._trustState = new WorkspaceTrustState(this.storageService); + this._trustTransitionManager = this._register(new WorkspaceTrustTransitionManager()); + this._trustStateInfo = this.loadTrustInfo(); - this._isWorkspaceTrusted = this.calculateWorkspaceTrust(); + this._trustState.isTrusted = this.calculateWorkspaceTrust(); this.registerListeners(); } - private set currentTrustState(trusted: boolean) { - if (this._isWorkspaceTrusted === trusted) { return; } - this._isWorkspaceTrusted = trusted; - - this._onDidChangeTrust.fire(trusted); - } + //#region private interface private registerListeners(): void { - this._register(this.workspaceService.onDidChangeWorkspaceFolders(() => this.currentTrustState = this.calculateWorkspaceTrust())); - this._register(this.workspaceService.onDidChangeWorkbenchState(() => this.currentTrustState = this.calculateWorkspaceTrust())); - this._register(this.storageService.onDidChangeValue(changeEvent => { - if (changeEvent.key === this.storageKey) { - this._trustStateInfo = this.loadTrustInfo(); - this.currentTrustState = this.calculateWorkspaceTrust(); + // Resolve the workspace uris and resolve the initialization promise + this.resolveCanonicalWorkspaceUris().then(async () => { + this._initialized = true; + await this.updateWorkspaceTrust(); + + this._workspaceResolvedPromiseResolve(); + if (!this.environmentService.remoteAuthority) { + this._workspaceTrustInitializedPromiseResolve(); + } + }); + + // Remote - resolve remote authority + if (this.environmentService.remoteAuthority) { + this.remoteAuthorityResolverService.resolveAuthority(this.environmentService.remoteAuthority) + .then(async result => { + this._remoteAuthority = result; + await this.updateWorkspaceTrust(); + + this._workspaceTrustInitializedPromiseResolve(); + }); + } + + this._register(this.workspaceService.onDidChangeWorkspaceFolders(async () => await this.updateWorkspaceTrust())); + this._register(this.workspaceService.onDidChangeWorkbenchState(async () => await this.updateWorkspaceTrust())); + this._register(this.storageService.onDidChangeValue(async changeEvent => { + /* This will only execute if storage was changed by a user action in a separate window */ + if (changeEvent.key === this.storageKey && JSON.stringify(this._trustStateInfo) !== JSON.stringify(this.loadTrustInfo())) { + this._trustStateInfo = this.loadTrustInfo(); this._onDidChangeTrustedFolders.fire(); + + await this.updateWorkspaceTrust(); } })); } + private async getCanonicalUri(uri: URI): Promise { + if (this.environmentService.remoteAuthority && uri.scheme === Schemas.vscodeRemote) { + return this.remoteAuthorityResolverService.getCanonicalURI(uri); + } + + if (uri.scheme === 'vscode-vfs') { + const index = uri.authority.indexOf('+'); + if (index !== -1) { + return uri.with({ authority: uri.authority.substr(0, index) }); + } + } + + return uri; + } + + private async resolveCanonicalWorkspaceUris(): Promise { + const workspaceUris = this.workspaceService.getWorkspace().folders.map(f => f.uri); + const canonicalWorkspaceFolders = await Promise.all(workspaceUris.map(uri => this.getCanonicalUri(uri))); + + let canonicalWorkspaceConfiguration = this.workspaceService.getWorkspace().configuration; + if (canonicalWorkspaceConfiguration && !isUntitledWorkspace(canonicalWorkspaceConfiguration, this.environmentService)) { + canonicalWorkspaceConfiguration = await this.getCanonicalUri(canonicalWorkspaceConfiguration); + } + + this._canonicalWorkspace = new CanonicalWorkspace(this.workspaceService.getWorkspace(), canonicalWorkspaceFolders, canonicalWorkspaceConfiguration); + } + private loadTrustInfo(): IWorkspaceTrustInfo { const infoAsString = this.storageService.get(this.storageKey, StorageScope.GLOBAL); @@ -115,12 +213,25 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork return result; } - private saveTrustInfo(): void { + private async saveTrustInfo(): Promise { this.storageService.store(this.storageKey, JSON.stringify(this._trustStateInfo), StorageScope.GLOBAL, StorageTarget.MACHINE); + this._onDidChangeTrustedFolders.fire(); + + await this.updateWorkspaceTrust(); + } + + private getWorkspaceUris(): URI[] { + const workspaceUris = this._canonicalWorkspace.folders.map(f => f.uri); + const workspaceConfiguration = this._canonicalWorkspace.configuration; + if (workspaceConfiguration && !isUntitledWorkspace(workspaceConfiguration, this.environmentService)) { + workspaceUris.push(workspaceConfiguration); + } + + return workspaceUris; } private calculateWorkspaceTrust(): boolean { - if (!isWorkspaceTrustEnabled(this.configurationService)) { + if (!this.workspaceTrustEnabled) { return true; } @@ -128,20 +239,55 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork return true; // trust running tests with vscode-test } - if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) { - return true; + // Remote - remote authority explicitly sets workspace trust + if (this.environmentService.remoteAuthority && this._remoteAuthority?.options?.isTrusted !== undefined) { + return this._remoteAuthority.options.isTrusted; } - const workspaceFolders = this.getWorkspaceFolders(); - const trusted = this.getFoldersTrust(workspaceFolders); + if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) { + // Use memento if present, otherwise default to restricted mode + // Workspace may transition to trusted based on the opened editors + return this._trustState.isTrusted ?? false; + } - return trusted; + if (!this._initialized) { + return false; + } + + return this.getUrisTrust(this.getWorkspaceUris()); } - private getFoldersTrust(folders: URI[]): boolean { + private async updateWorkspaceTrust(trusted?: boolean): Promise { + if (!this.workspaceTrustEnabled) { + return; + } + + if (trusted === undefined) { + await this.resolveCanonicalWorkspaceUris(); + trusted = this.calculateWorkspaceTrust(); + } + + if (this.isWorkpaceTrusted() === trusted) { return; } + + // Update workspace trust + this._trustState.isTrusted = trusted; + + // Reset acceptsOutOfWorkspaceFiles + if (!trusted) { + this._trustState.acceptsOutOfWorkspaceFiles = false; + } + + // Run workspace trust transition participants + await this._trustTransitionManager.participate(trusted); + + // Fire workspace trust change event + this._onDidChangeTrust.fire(trusted); + } + + private getUrisTrust(uris: URI[]): boolean { let state = true; - for (const folder of folders) { - const { trusted } = this.getFolderTrustInfo(folder); + for (const uri of uris) { + const { trusted } = this.doGetUriTrustInfo(uri); if (!trusted) { state = trusted; @@ -152,24 +298,23 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork return state; } - private getWorkspaceFolders(): URI[] { - const folderURIs = this.workspaceService.getWorkspace().folders.map(f => f.uri); - const workspaceConfiguration = this.workspaceService.getWorkspace().configuration; - if (workspaceConfiguration && !isUntitledWorkspace(workspaceConfiguration, this.environmentService)) { - folderURIs.push(dirname(workspaceConfiguration)); + private doGetUriTrustInfo(uri: URI): IWorkspaceTrustUriInfo { + // Return trusted when workspace trust is disabled + if (!this.workspaceTrustEnabled) { + return { trusted: true, uri }; } - return folderURIs; - } + if (this.isTrustedVirtualResource(uri)) { + return { trusted: true, uri }; + } - public getFolderTrustInfo(folder: URI): IWorkspaceTrustUriInfo { let resultState = false; let maxLength = -1; - let resultUri = folder; + let resultUri = uri; for (const trustInfo of this._trustStateInfo.uriTrustInfo) { - if (this.uriIdentityService.extUri.isEqualOrParent(folder, trustInfo.uri)) { + if (this.uriIdentityService.extUri.isEqualOrParent(uri, trustInfo.uri)) { const fsPath = trustInfo.uri.fsPath; if (fsPath.length > maxLength) { maxLength = fsPath.length; @@ -182,19 +327,23 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork return { trusted: resultState, uri: resultUri }; } - setFoldersTrust(folders: URI[], trusted: boolean): void { + private async doSetUrisTrust(uris: URI[], trusted: boolean): Promise { let changed = false; - for (const folder of folders) { + for (const uri of uris) { if (trusted) { - const foundItem = this._trustStateInfo.uriTrustInfo.find(trustInfo => this.uriIdentityService.extUri.isEqual(trustInfo.uri, folder)); + if (this.isTrustedVirtualResource(uri)) { + continue; + } + + const foundItem = this._trustStateInfo.uriTrustInfo.find(trustInfo => this.uriIdentityService.extUri.isEqual(trustInfo.uri, uri)); if (!foundItem) { - this._trustStateInfo.uriTrustInfo.push({ uri: folder, trusted: true }); + this._trustStateInfo.uriTrustInfo.push({ uri, trusted: true }); changed = true; } } else { const previousLength = this._trustStateInfo.uriTrustInfo.length; - this._trustStateInfo.uriTrustInfo = this._trustStateInfo.uriTrustInfo.filter(trustInfo => !this.uriIdentityService.extUri.isEqual(trustInfo.uri, folder)); + this._trustStateInfo.uriTrustInfo = this._trustStateInfo.uriTrustInfo.filter(trustInfo => !this.uriIdentityService.extUri.isEqual(trustInfo.uri, uri)); if (previousLength !== this._trustStateInfo.uriTrustInfo.length) { changed = true; } @@ -202,81 +351,210 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork } if (changed) { - this.saveTrustInfo(); + await this.saveTrustInfo(); + } + } + + private isTrustedVirtualResource(uri: URI): boolean { + return isVirtualResource(uri) && uri.scheme !== 'vscode-vfs'; + } + + //#endregion + + //#region public interface + + get workspaceResolved(): Promise { + return this._workspaceResolvedPromise; + } + + get workspaceTrustEnabled(): boolean { + if (this.environmentService.disableWorkspaceTrust) { + return false; + } + + return this.configurationService.getValue(WORKSPACE_TRUST_ENABLED) ?? false; + } + + get workspaceTrustInitialized(): Promise { + return this._workspaceTrustInitializedPromise; + } + + get acceptsOutOfWorkspaceFiles(): boolean { + return this._trustState.acceptsOutOfWorkspaceFiles; + } + + set acceptsOutOfWorkspaceFiles(value: boolean) { + this._trustState.acceptsOutOfWorkspaceFiles = value; + } + + isWorkpaceTrusted(): boolean { + return this._trustState.isTrusted ?? false; + } + + isWorkspaceTrustForced(): boolean { + // Remote - remote authority explicitly sets workspace trust + if (this.environmentService.remoteAuthority && this._remoteAuthority && this._remoteAuthority.options?.isTrusted !== undefined) { + return true; + } + + // All workspace uris are trusted automatically + const workspaceUris = this.getWorkspaceUris().filter(uri => !this.isTrustedVirtualResource(uri)); + if (workspaceUris.length === 0) { + return true; + } + + return false; + } + + canSetParentFolderTrust(): boolean { + const workspaceIdentifier = toWorkspaceIdentifier(this._canonicalWorkspace); + return isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && workspaceIdentifier.uri.scheme === Schemas.file; + } + + async setParentFolderTrust(trusted: boolean): Promise { + const workspaceIdentifier = toWorkspaceIdentifier(this._canonicalWorkspace); + if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && workspaceIdentifier.uri.scheme === Schemas.file) { + const { parentPath } = splitName(workspaceIdentifier.uri.fsPath); + + await this.setUrisTrust([URI.file(parentPath)], trusted); } } canSetWorkspaceTrust(): boolean { - return this.workspaceService.getWorkbenchState() !== WorkbenchState.EMPTY; - } - - canSetParentFolderTrust(): boolean { - const workspaceIdentifier = toWorkspaceIdentifier(this.workspaceService.getWorkspace()); - return isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && workspaceIdentifier.uri.scheme === Schemas.file; - } - - isWorkpaceTrusted(): boolean { - return this._isWorkspaceTrusted; - } - - setParentFolderTrust(trusted: boolean): void { - const workspaceIdentifier = toWorkspaceIdentifier(this.workspaceService.getWorkspace()); - if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && workspaceIdentifier.uri.scheme === Schemas.file) { - const { parentPath } = splitName(workspaceIdentifier.uri.fsPath); - - this.setFoldersTrust([URI.file(parentPath)], trusted); + // Remote - remote authority not yet resolved, or remote authority explicitly sets workspace trust + if (this.environmentService.remoteAuthority && (!this._remoteAuthority || this._remoteAuthority.options?.isTrusted !== undefined)) { + return false; } + + // Empty workspace + if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) { + return true; + } + + // All workspace uris are trusted automatically + const workspaceUris = this.getWorkspaceUris().filter(uri => !this.isTrustedVirtualResource(uri)); + if (workspaceUris.length === 0) { + return false; + } + + // Untrusted workspace + if (!this.isWorkpaceTrusted()) { + return true; + } + + // Trusted workspace + // Can only be trusted explicitly in the single folder scenario + const workspaceIdentifier = toWorkspaceIdentifier(this._canonicalWorkspace); + if (!(isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && workspaceIdentifier.uri.scheme === Schemas.file)) { + return false; + } + + // If the current folder isn't trusted directly, return false + const trustInfo = this.doGetUriTrustInfo(workspaceIdentifier.uri); + if (!trustInfo.trusted || !this.uriIdentityService.extUri.isEqual(workspaceIdentifier.uri, trustInfo.uri)) { + return false; + } + + // Check if the parent is also trusted + if (this.canSetParentFolderTrust()) { + const { parentPath } = splitName(workspaceIdentifier.uri.fsPath); + const parentPathTrustInfo = this.doGetUriTrustInfo(URI.file(parentPath)); + if (parentPathTrustInfo.trusted) { + return false; + } + } + + return true; } - setWorkspaceTrust(trusted: boolean): void { - const workspaceFolders = this.getWorkspaceFolders(); - this.setFoldersTrust(workspaceFolders, trusted); + async setWorkspaceTrust(trusted: boolean): Promise { + // Empty workspace + if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) { + await this.updateWorkspaceTrust(trusted); + return; + } + + const workspaceFolders = this.getWorkspaceUris(); + await this.setUrisTrust(workspaceFolders, trusted); } - getTrustedFolders(): URI[] { + async getUriTrustInfo(uri: URI): Promise { + // Return trusted when workspace trust is disabled + if (!this.workspaceTrustEnabled) { + return { trusted: true, uri }; + } + + return this.doGetUriTrustInfo(await this.getCanonicalUri(uri)); + } + + async setUrisTrust(uris: URI[], trusted: boolean): Promise { + this.doSetUrisTrust(await Promise.all(uris.map(uri => this.getCanonicalUri(uri))), trusted); + } + + getTrustedUris(): URI[] { return this._trustStateInfo.uriTrustInfo.map(info => info.uri); } - setTrustedFolders(folders: URI[]): void { + async setTrustedUris(uris: URI[]): Promise { this._trustStateInfo.uriTrustInfo = []; - for (const folder of folders) { + for (const uri of uris) { + const canonicalUri = await this.getCanonicalUri(uri); + const cleanUri = this.uriIdentityService.extUri.removeTrailingPathSeparator(canonicalUri); + let added = false; + for (const addedUri of this._trustStateInfo.uriTrustInfo) { + if (this.uriIdentityService.extUri.isEqual(addedUri.uri, cleanUri)) { + added = true; + break; + } + } + + if (added) { + continue; + } + this._trustStateInfo.uriTrustInfo.push({ trusted: true, - uri: folder + uri: cleanUri }); } - this.saveTrustInfo(); + await this.saveTrustInfo(); } + + addWorkspaceTrustTransitionParticipant(participant: IWorkspaceTrustTransitionParticipant): IDisposable { + return this._trustTransitionManager.addWorkspaceTrustTransitionParticipant(participant); + } + + //#endregion } export class WorkspaceTrustRequestService extends Disposable implements IWorkspaceTrustRequestService { _serviceBrand: undefined; private _trusted!: boolean; - private _trustRequestPromise?: Promise; - private _trustRequestResolver?: (trusted: boolean) => void; private _modalTrustRequestPromise?: Promise; private _modalTrustRequestResolver?: (trusted: boolean | undefined) => void; + private readonly _ctxWorkspaceTrustEnabled: IContextKey; private readonly _ctxWorkspaceTrustState: IContextKey; - private readonly _ctxWorkspaceTrustPendingRequest: IContextKey; - private readonly _onDidInitiateWorkspaceTrustRequest = this._register(new Emitter()); + private readonly _onDidInitiateWorkspaceTrustRequest = this._register(new Emitter()); readonly onDidInitiateWorkspaceTrustRequest = this._onDidInitiateWorkspaceTrustRequest.event; - private readonly _onDidCompleteWorkspaceTrustRequest = this._register(new Emitter()); - readonly onDidCompleteWorkspaceTrustRequest = this._onDidCompleteWorkspaceTrustRequest.event; constructor( @IContextKeyService contextKeyService: IContextKeyService, - @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, + @IDialogService private readonly dialogService: IDialogService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService ) { super(); - this._register(this.workspaceTrustManagementService.onDidChangeTrust(trusted => this.onTrustStateChanged(trusted))); + this._register(this.workspaceTrustManagementService.onDidChangeTrust(trusted => this.trusted = trusted)); + this._ctxWorkspaceTrustEnabled = WorkspaceTrustContext.IsEnabled.bindTo(contextKeyService); this._ctxWorkspaceTrustState = WorkspaceTrustContext.IsTrusted.bindTo(contextKeyService); - this._ctxWorkspaceTrustPendingRequest = WorkspaceTrustContext.PendingRequest.bindTo(contextKeyService); + this._ctxWorkspaceTrustEnabled.set(this.workspaceTrustManagementService.workspaceTrustEnabled); this.trusted = this.workspaceTrustManagementService.isWorkpaceTrusted(); } @@ -290,21 +568,21 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa this._ctxWorkspaceTrustState.set(trusted); } - private onTrustStateChanged(trusted: boolean): void { - // Resolve any pending soft requests for workspace trust - if (this._trustRequestResolver) { - this._trustRequestResolver(trusted); + private get untrustedFilesSetting(): 'prompt' | 'open' | 'newWindow' { + return this.configurationService.getValue(WORKSPACE_TRUST_UNTRUSTED_FILES); + } - this._trustRequestResolver = undefined; - this._trustRequestPromise = undefined; + private set untrustedFilesSetting(value: 'prompt' | 'open' | 'newWindow') { + this.configurationService.updateValue(WORKSPACE_TRUST_UNTRUSTED_FILES, value); + } + + private resolveRequest(trusted?: boolean): void { + if (this._modalTrustRequestResolver) { + this._modalTrustRequestResolver(trusted ?? this.trusted); + + this._modalTrustRequestResolver = undefined; + this._modalTrustRequestPromise = undefined; } - - // Update context if there are no pending requests - if (!this._modalTrustRequestPromise && !this._trustRequestPromise) { - this._ctxWorkspaceTrustPendingRequest.set(false); - } - - this.trusted = trusted; } cancelRequest(): void { @@ -316,62 +594,160 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa } } - completeRequest(trusted?: boolean): void { - if (this._modalTrustRequestResolver) { - this._modalTrustRequestResolver(trusted ?? this.trusted); - - this._modalTrustRequestResolver = undefined; - this._modalTrustRequestPromise = undefined; - } - if (this._trustRequestResolver) { - this._trustRequestResolver(trusted ?? this.trusted); - - this._trustRequestResolver = undefined; - this._trustRequestPromise = undefined; - } - - if (trusted === undefined) { + async completeRequest(trusted?: boolean): Promise { + if (trusted === undefined || trusted === this.trusted) { + this.resolveRequest(trusted); return; } - this.workspaceTrustManagementService.setWorkspaceTrust(trusted); - this._onDidCompleteWorkspaceTrustRequest.fire(trusted); + // Update storage, transition workspace, and resolve the promise + await this.workspaceTrustManagementService.setWorkspaceTrust(trusted); + this.resolveRequest(trusted); } - async requestWorkspaceTrust(options: WorkspaceTrustRequestOptions = { modal: false }): Promise { + async requestOpenUris(uris: URI[]): Promise { + // If workspace is untrusted, there is no conflict + if (!this.trusted) { + return WorkspaceTrustUriResponse.Open; + } + + const openFilesTrustInfo = await Promise.all(uris.map(uri => this.workspaceTrustManagementService.getUriTrustInfo(uri))); + + // If all uris are trusted, there is no conflict + if (openFilesTrustInfo.map(info => info.trusted).every(trusted => trusted)) { + return WorkspaceTrustUriResponse.Open; + } + + // If user has setting, don't need to ask + if (this.untrustedFilesSetting !== 'prompt') { + if (this.untrustedFilesSetting === 'newWindow') { + return WorkspaceTrustUriResponse.OpenInNewWindow; + } + + if (this.untrustedFilesSetting === 'open') { + return WorkspaceTrustUriResponse.Open; + } + } + + // If we already asked the user, don't need to ask again + if (this.workspaceTrustManagementService.acceptsOutOfWorkspaceFiles) { + return WorkspaceTrustUriResponse.Open; + } + + const markdownDetails = [ + this.workspaceService.getWorkbenchState() !== WorkbenchState.EMPTY ? + localize('openLooseFileWorkspaceDetails', "You are trying to open untrusted files in a workspace which is trusted.") : + localize('openLooseFileWindowDetails', "You are trying to open untrusted files in a window which is trusted."), + localize('openLooseFileLearnMore', "If you don't trust the authors of these files, we recommend to open them in Restricted Mode in a new window as the files may be malicious. See [our docs](https://aka.ms/vscode-workspace-trust) to learn more.") + ]; + + const result = await this.dialogService.show(Severity.Info, localize('openLooseFileMesssage', "Do you trust the authors of these files?"), [localize('open', "Open"), localize('newWindow', "Open in Restricted Mode"), localize('cancel', "Cancel")], { + cancelId: 2, + checkbox: { + label: localize('openLooseFileWorkspaceCheckbox', "Remember my decision for all workspaces"), + checked: false + }, + custom: { + icon: Codicon.shield, + markdownDetails: markdownDetails.map(md => { return { markdown: new MarkdownString(md) }; }) + } + }); + + const saveResponseIfChecked = (response: WorkspaceTrustUriResponse, checked: boolean) => { + if (checked) { + if (response === WorkspaceTrustUriResponse.Open) { + this.untrustedFilesSetting = 'open'; + } + + if (response === WorkspaceTrustUriResponse.OpenInNewWindow) { + this.untrustedFilesSetting = 'newWindow'; + } + } + + return response; + }; + + switch (result.choice) { + case 0: + this.workspaceTrustManagementService.acceptsOutOfWorkspaceFiles = true; + return saveResponseIfChecked(WorkspaceTrustUriResponse.Open, !!result.checkboxChecked); + case 1: + return saveResponseIfChecked(WorkspaceTrustUriResponse.OpenInNewWindow, !!result.checkboxChecked); + default: + return WorkspaceTrustUriResponse.Cancel; + } + } + + async requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise { // Trusted workspace if (this.trusted) { return this.trusted; } - if (options.modal) { - // Modal request - if (!this._modalTrustRequestPromise) { - // Create promise - this._modalTrustRequestPromise = new Promise(resolve => { - this._modalTrustRequestResolver = resolve; - }); - } else { - // Return existing promise - return this._modalTrustRequestPromise; - } + // Modal request + if (!this._modalTrustRequestPromise) { + // Create promise + this._modalTrustRequestPromise = new Promise(resolve => { + this._modalTrustRequestResolver = resolve; + }); } else { - // Soft request - if (!this._trustRequestPromise) { - // Create promise - this._trustRequestPromise = new Promise(resolve => { - this._trustRequestResolver = resolve; - }); - } else { - // Return existing promise - return this._trustRequestPromise; - } + // Return existing promise + return this._modalTrustRequestPromise; } - this._ctxWorkspaceTrustPendingRequest.set(true); this._onDidInitiateWorkspaceTrustRequest.fire(options); + return this._modalTrustRequestPromise; + } +} - return options.modal ? this._modalTrustRequestPromise! : this._trustRequestPromise!; +class WorkspaceTrustTransitionManager extends Disposable { + + private readonly participants = new LinkedList(); + + addWorkspaceTrustTransitionParticipant(participant: IWorkspaceTrustTransitionParticipant): IDisposable { + const remove = this.participants.push(participant); + return toDisposable(() => remove()); + } + + async participate(trusted: boolean): Promise { + for (const participant of this.participants) { + await participant.participate(trusted); + } + } + + override dispose(): void { + this.participants.clear(); + } +} + +class WorkspaceTrustState { + private readonly _memento: Memento; + private readonly _mementoObject: MementoObject; + + private readonly _acceptsOutOfWorkspaceFilesKey = 'acceptsOutOfWorkspaceFiles'; + private readonly _isTrustedKey = 'isTrusted'; + + constructor(storageService: IStorageService) { + this._memento = new Memento('workspaceTrust', storageService); + this._mementoObject = this._memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + + get acceptsOutOfWorkspaceFiles(): boolean { + return this._mementoObject[this._acceptsOutOfWorkspaceFilesKey] ?? false; + } + + set acceptsOutOfWorkspaceFiles(value: boolean) { + this._mementoObject[this._acceptsOutOfWorkspaceFilesKey] = value; + this._memento.saveMemento(); + } + + get isTrusted(): boolean | undefined { + return this._mementoObject[this._isTrustedKey]; + } + + set isTrusted(value: boolean | undefined) { + this._mementoObject[this._isTrustedKey] = value; + this._memento.saveMemento(); } } diff --git a/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts b/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts index 6bc5893c7c..fe90e449b9 100644 --- a/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts @@ -147,7 +147,7 @@ export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingServi await this.dialogService.show( Severity.Info, localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(path)), - [localize('ok', "OK")], + undefined, { detail: localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again.") } diff --git a/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts b/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts index d3d696eb9a..6fe7af8c22 100644 --- a/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts +++ b/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts @@ -1,11 +1,12 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Emitter } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, IWorkspaceTrustUriInfo, WorkspaceTrustRequestOptions } from 'vs/platform/workspace/common/workspaceTrust'; +import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, IWorkspaceTrustTransitionParticipant, IWorkspaceTrustUriInfo, WorkspaceTrustRequestOptions, WorkspaceTrustUriResponse } from 'vs/platform/workspace/common/workspaceTrust'; export class TestWorkspaceTrustManagementService implements IWorkspaceTrustManagementService { @@ -17,29 +18,44 @@ export class TestWorkspaceTrustManagementService implements IWorkspaceTrustManag private _onDidChangeTrustedFolders = new Emitter(); onDidChangeTrustedFolders = this._onDidChangeTrustedFolders.event; - private trusted: boolean; + private _onDidInitiateWorkspaceTrustRequestOnStartup = new Emitter(); + onDidInitiateWorkspaceTrustRequestOnStartup = this._onDidInitiateWorkspaceTrustRequestOnStartup.event; - constructor(trusted: boolean = true) { - this.trusted = trusted; + + constructor( + private enabled: boolean = true, + private trusted: boolean = true) { } - getTrustedFolders(): URI[] { + get acceptsOutOfWorkspaceFiles(): boolean { throw new Error('Method not implemented.'); } - setParentFolderTrust(trusted: boolean): void { + set acceptsOutOfWorkspaceFiles(value: boolean) { throw new Error('Method not implemented.'); } - getFolderTrustInfo(folder: URI): IWorkspaceTrustUriInfo { + addWorkspaceTrustTransitionParticipant(participant: IWorkspaceTrustTransitionParticipant): IDisposable { throw new Error('Method not implemented.'); } - setTrustedFolders(folders: URI[]): void { + getTrustedUris(): URI[] { throw new Error('Method not implemented.'); } - setFoldersTrust(folders: URI[], trusted: boolean): void { + setParentFolderTrust(trusted: boolean): Promise { + throw new Error('Method not implemented.'); + } + + getUriTrustInfo(uri: URI): Promise { + throw new Error('Method not implemented.'); + } + + async setTrustedUris(folders: URI[]): Promise { + throw new Error('Method not implemented.'); + } + + async setUrisTrust(uris: URI[], trusted: boolean): Promise { throw new Error('Method not implemented.'); } @@ -55,7 +71,23 @@ export class TestWorkspaceTrustManagementService implements IWorkspaceTrustManag return this.trusted; } - setWorkspaceTrust(trusted: boolean): void { + isWorkspaceTrustForced(): boolean { + return false; + } + + get workspaceTrustEnabled(): boolean { + return this.enabled; + } + + get workspaceTrustInitialized(): Promise { + return Promise.resolve(); + } + + get workspaceResolved(): Promise { + return Promise.resolve(); + } + + async setWorkspaceTrust(trusted: boolean): Promise { if (this.trusted !== trusted) { this.trusted = trusted; this._onDidChangeTrust.fire(this.trusted); @@ -74,11 +106,19 @@ export class TestWorkspaceTrustRequestService implements IWorkspaceTrustRequestS constructor(private readonly _trusted: boolean) { } + requestOpenUrisHandler = async (uris: URI[]) => { + return WorkspaceTrustUriResponse.Open; + }; + + requestOpenUris(uris: URI[]): Promise { + return this.requestOpenUrisHandler(uris); + } + cancelRequest(): void { throw new Error('Method not implemented.'); } - completeRequest(trusted?: boolean): void { + async completeRequest(trusted?: boolean): Promise { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts b/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts index 9566955d99..5eaa357b1e 100644 --- a/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts +++ b/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts @@ -49,7 +49,7 @@ import 'vs/editor/contrib/parameterHints/provideSignatureHelp'; import 'vs/editor/contrib/smartSelect/smartSelect'; import 'vs/editor/contrib/suggest/suggest'; import 'vs/editor/contrib/rename/rename'; -import 'vs/editor/contrib/inlineHints/inlineHintsController'; +import 'vs/editor/contrib/inlayHints/inlayHintsController'; const defaultSelector = { scheme: 'far' }; const model: ITextModel = createTextModel( @@ -1204,85 +1204,74 @@ suite('ExtHostLanguageFeatureCommands', function () { // --- inline hints - test('Inline Hints, back and forth', async function () { - disposables.push(extHost.registerInlineHintsProvider(nullExtensionDescription, defaultSelector, { - provideInlineHints() { - return [new types.InlineHint('Foo', new types.Range(0, 1, 2, 3))]; + test('Inlay Hints, back and forth', async function () { + disposables.push(extHost.registerInlayHintsProvider(nullExtensionDescription, defaultSelector, { + provideInlayHints() { + return [new types.InlayHint('Foo', new types.Position(0, 1))]; } })); await rpcProtocol.sync(); - const value = await commands.executeCommand('vscode.executeInlineHintProvider', model.uri, new types.Range(0, 0, 20, 20)); + const value = await commands.executeCommand('vscode.executeInlayHintProvider', model.uri, new types.Range(0, 0, 20, 20)); assert.strictEqual(value.length, 1); const [first] = value; assert.strictEqual(first.text, 'Foo'); - assert.strictEqual(first.range.start.line, 0); - assert.strictEqual(first.range.start.character, 1); - assert.strictEqual(first.range.end.line, 2); - assert.strictEqual(first.range.end.character, 3); + assert.strictEqual(first.position.line, 0); + assert.strictEqual(first.position.character, 1); }); test('Inline Hints, merge', async function () { - disposables.push(extHost.registerInlineHintsProvider(nullExtensionDescription, defaultSelector, { - provideInlineHints() { - return [new types.InlineHint('Bar', new types.Range(10, 11, 12, 13))]; + disposables.push(extHost.registerInlayHintsProvider(nullExtensionDescription, defaultSelector, { + provideInlayHints() { + return [new types.InlayHint('Bar', new types.Position(10, 11))]; } })); - disposables.push(extHost.registerInlineHintsProvider(nullExtensionDescription, defaultSelector, { - provideInlineHints() { - const hint = new types.InlineHint('Foo', new types.Range(0, 1, 2, 3), types.InlineHintKind.Parameter); - hint.description = new types.MarkdownString('**Hello**'); + disposables.push(extHost.registerInlayHintsProvider(nullExtensionDescription, defaultSelector, { + provideInlayHints() { + const hint = new types.InlayHint('Foo', new types.Position(0, 1), types.InlayHintKind.Parameter); return [hint]; } })); await rpcProtocol.sync(); - const value = await commands.executeCommand('vscode.executeInlineHintProvider', model.uri, new types.Range(0, 0, 20, 20)); + const value = await commands.executeCommand('vscode.executeInlayHintProvider', model.uri, new types.Range(0, 0, 20, 20)); assert.strictEqual(value.length, 2); const [first, second] = value; assert.strictEqual(first.text, 'Foo'); - assert.strictEqual(first.range.start.line, 0); - assert.strictEqual(first.range.start.character, 1); - assert.strictEqual(first.range.end.line, 2); - assert.strictEqual(first.range.end.character, 3); - assert.ok(first.description instanceof types.MarkdownString); - assert.strictEqual((first.description).value, '**Hello**'); + assert.strictEqual(first.position.line, 0); + assert.strictEqual(first.position.character, 1); assert.strictEqual(second.text, 'Bar'); - assert.strictEqual(second.range.start.line, 10); - assert.strictEqual(second.range.start.character, 11); - assert.strictEqual(second.range.end.line, 12); - assert.strictEqual(second.range.end.character, 13); + assert.strictEqual(second.position.line, 10); + assert.strictEqual(second.position.character, 11); }); test('Inline Hints, bad provider', async function () { - disposables.push(extHost.registerInlineHintsProvider(nullExtensionDescription, defaultSelector, { - provideInlineHints() { - return [new types.InlineHint('Foo', new types.Range(0, 1, 2, 3))]; + disposables.push(extHost.registerInlayHintsProvider(nullExtensionDescription, defaultSelector, { + provideInlayHints() { + return [new types.InlayHint('Foo', new types.Position(0, 1))]; } })); - disposables.push(extHost.registerInlineHintsProvider(nullExtensionDescription, defaultSelector, { - provideInlineHints() { + disposables.push(extHost.registerInlayHintsProvider(nullExtensionDescription, defaultSelector, { + provideInlayHints() { throw new Error(); } })); await rpcProtocol.sync(); - const value = await commands.executeCommand('vscode.executeInlineHintProvider', model.uri, new types.Range(0, 0, 20, 20)); + const value = await commands.executeCommand('vscode.executeInlayHintProvider', model.uri, new types.Range(0, 0, 20, 20)); assert.strictEqual(value.length, 1); const [first] = value; assert.strictEqual(first.text, 'Foo'); - assert.strictEqual(first.range.start.line, 0); - assert.strictEqual(first.range.start.character, 1); - assert.strictEqual(first.range.end.line, 2); - assert.strictEqual(first.range.end.character, 3); + assert.strictEqual(first.position.line, 0); + assert.strictEqual(first.position.character, 1); }); // --- selection ranges diff --git a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts index 7b6218db9a..93736c4ea9 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts @@ -13,7 +13,7 @@ import { mock } from 'vs/base/test/common/mock'; import { IModelAddedData, MainContext, MainThreadCommandsShape, MainThreadNotebookShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; import { ExtHostNotebookDocument } from 'vs/workbench/api/common/extHostNotebookDocument'; -import { CellKind, CellUri, NotebookCellExecutionState, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellUri, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { URI } from 'vs/base/common/uri'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; @@ -22,6 +22,7 @@ import { isEqual } from 'vs/base/common/resources'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import { generateUuid } from 'vs/base/common/uuid'; import { Event } from 'vs/base/common/event'; +import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNotebookDocuments'; suite('NotebookCell#Document', function () { @@ -31,6 +32,8 @@ suite('NotebookCell#Document', function () { let extHostDocumentsAndEditors: ExtHostDocumentsAndEditors; let extHostDocuments: ExtHostDocuments; let extHostNotebooks: ExtHostNotebookController; + let extHostNotebookDocuments: ExtHostNotebookDocuments; + const notebookUri = URI.parse('test:///notebook.file'); const disposables = new DisposableStore(); @@ -54,7 +57,9 @@ suite('NotebookCell#Document', function () { return URI.from({ scheme: 'test', path: generateUuid() }); } }; - extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors, extHostDocuments, new NullLogService(), extHostStoragePaths); + extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors, extHostDocuments, extHostStoragePaths); + extHostNotebookDocuments = new ExtHostNotebookDocuments(new NullLogService(), extHostNotebooks); + let reg = extHostNotebooks.registerNotebookContentProvider(nullExtensionDescription, 'test', new class extends mock() { // async openNotebook() { } }); @@ -69,7 +74,7 @@ suite('NotebookCell#Document', function () { source: ['### Heading'], eol: '\n', language: 'markdown', - cellKind: CellKind.Markdown, + cellKind: CellKind.Markup, outputs: [], }, { handle: 1, @@ -162,7 +167,7 @@ suite('NotebookCell#Document', function () { }); }); - extHostNotebooks.$acceptModelChanged(notebookUri, { + extHostNotebookDocuments.$acceptModelChanged(notebookUri, { versionId: notebook.apiNotebook.version + 1, rawEvents: [ { @@ -238,7 +243,7 @@ suite('NotebookCell#Document', function () { assert.strictEqual(notebook.apiNotebook.cellCount, 2); const [cell1, cell2] = notebook.apiNotebook.getCells(); - extHostNotebooks.$acceptModelChanged(notebook.uri, { + extHostNotebookDocuments.$acceptModelChanged(notebook.uri, { versionId: 2, rawEvents: [ { @@ -269,7 +274,7 @@ suite('NotebookCell#Document', function () { assert.strictEqual(second.index, 1); // remove first cell - extHostNotebooks.$acceptModelChanged(notebook.uri, { + extHostNotebookDocuments.$acceptModelChanged(notebook.uri, { versionId: notebook.apiNotebook.version + 1, rawEvents: [{ kind: NotebookCellsChangeType.ModelChange, @@ -280,7 +285,7 @@ suite('NotebookCell#Document', function () { assert.strictEqual(notebook.apiNotebook.cellCount, 1); assert.strictEqual(second.index, 0); - extHostNotebooks.$acceptModelChanged(notebookUri, { + extHostNotebookDocuments.$acceptModelChanged(notebookUri, { versionId: notebook.apiNotebook.version + 1, rawEvents: [{ kind: NotebookCellsChangeType.ModelChange, @@ -315,7 +320,7 @@ suite('NotebookCell#Document', function () { // DON'T call this, make sure the cell-documents have not been created yet // assert.strictEqual(notebook.notebookDocument.cellCount, 2); - extHostNotebooks.$acceptModelChanged(notebook.uri, { + extHostNotebookDocuments.$acceptModelChanged(notebook.uri, { versionId: 100, rawEvents: [{ kind: NotebookCellsChangeType.ModelChange, @@ -325,7 +330,7 @@ suite('NotebookCell#Document', function () { source: ['### Heading'], eol: '\n', language: 'markdown', - cellKind: CellKind.Markdown, + cellKind: CellKind.Markup, outputs: [], }, { handle: 4, @@ -398,7 +403,7 @@ suite('NotebookCell#Document', function () { const removed = Event.toPromise(extHostDocuments.onDidRemoveDocument); const added = Event.toPromise(extHostDocuments.onDidAddDocument); - extHostNotebooks.$acceptModelChanged(notebook.uri, { + extHostNotebookDocuments.$acceptModelChanged(notebook.uri, { versionId: 12, rawEvents: [{ kind: NotebookCellsChangeType.ChangeLanguage, index: 0, @@ -412,30 +417,4 @@ suite('NotebookCell#Document', function () { assert.strictEqual(first.document.languageId, 'fooLang'); assert.ok(removedDoc === addedDoc); }); - - test('change cell execution state does not trigger onDidChangeMetadata event', async function () { - let didFireOnDidChangeMetadata = false; - let e = extHostNotebooks.onDidChangeCellMetadata(() => { - didFireOnDidChangeMetadata = true; - }); - - const changeExeState = Event.toPromise(extHostNotebooks.onDidChangeNotebookCellExecutionState); - - extHostNotebooks.$acceptModelChanged(notebook.uri, { - versionId: 12, rawEvents: [{ - kind: NotebookCellsChangeType.ChangeCellMetadata, - index: 0, - metadata: { - ...notebook.getCellFromIndex(0)?.internalMetadata, - ...{ - runState: NotebookCellExecutionState.Executing - } - } - }] - }, false); - - await changeExeState; - assert.strictEqual(didFireOnDidChangeMetadata, false); - e.dispose(); - }); }); diff --git a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts index 1d936d1ba6..cc0ba286b2 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts @@ -22,6 +22,7 @@ import { MainContext, MainThreadCommandsShape, MainThreadNotebookShape } from 'v import { DisposableStore } from 'vs/base/common/lifecycle'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import { generateUuid } from 'vs/base/common/uuid'; +import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNotebookDocuments'; suite('NotebookConcatDocument', function () { @@ -30,6 +31,8 @@ suite('NotebookConcatDocument', function () { let extHostDocumentsAndEditors: ExtHostDocumentsAndEditors; let extHostDocuments: ExtHostDocuments; let extHostNotebooks: ExtHostNotebookController; + let extHostNotebookDocuments: ExtHostNotebookDocuments; + const notebookUri = URI.parse('test:///notebook.file'); const disposables = new DisposableStore(); @@ -51,7 +54,9 @@ suite('NotebookConcatDocument', function () { return URI.from({ scheme: 'test', path: generateUuid() }); } }; - extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors, extHostDocuments, new NullLogService(), extHostStoragePaths); + extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors, extHostDocuments, extHostStoragePaths); + extHostNotebookDocuments = new ExtHostNotebookDocuments(new NullLogService(), extHostNotebooks); + let reg = extHostNotebooks.registerNotebookContentProvider(nullExtensionDescription, 'test', new class extends mock() { // async openNotebook() { } }); @@ -65,7 +70,7 @@ suite('NotebookConcatDocument', function () { source: ['### Heading'], eol: '\n', language: 'markdown', - cellKind: CellKind.Markdown, + cellKind: CellKind.Markup, outputs: [], }], versionId: 0 @@ -122,7 +127,7 @@ suite('NotebookConcatDocument', function () { const cellUri1 = CellUri.generate(notebook.uri, 1); const cellUri2 = CellUri.generate(notebook.uri, 2); - extHostNotebooks.$acceptModelChanged(notebookUri, { + extHostNotebookDocuments.$acceptModelChanged(notebookUri, { versionId: notebook.apiNotebook.version + 1, rawEvents: [{ kind: NotebookCellsChangeType.ModelChange, @@ -159,7 +164,7 @@ suite('NotebookConcatDocument', function () { test('location, position mapping', function () { - extHostNotebooks.$acceptModelChanged(notebookUri, { + extHostNotebookDocuments.$acceptModelChanged(notebookUri, { versionId: notebook.apiNotebook.version + 1, rawEvents: [ { @@ -204,7 +209,7 @@ suite('NotebookConcatDocument', function () { let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.apiNotebook, undefined); // UPDATE 1 - extHostNotebooks.$acceptModelChanged(notebookUri, { + extHostNotebookDocuments.$acceptModelChanged(notebookUri, { versionId: notebook.apiNotebook.version + 1, rawEvents: [ { @@ -231,7 +236,7 @@ suite('NotebookConcatDocument', function () { // UPDATE 2 - extHostNotebooks.$acceptModelChanged(notebookUri, { + extHostNotebookDocuments.$acceptModelChanged(notebookUri, { versionId: notebook.apiNotebook.version + 1, rawEvents: [ { @@ -259,7 +264,7 @@ suite('NotebookConcatDocument', function () { assertLocation(doc, new Position(5, 12), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(2, 11)), false); // don't check identity because position will be clamped // UPDATE 3 (remove cell #2 again) - extHostNotebooks.$acceptModelChanged(notebookUri, { + extHostNotebookDocuments.$acceptModelChanged(notebookUri, { versionId: notebook.apiNotebook.version + 1, rawEvents: [ { @@ -281,7 +286,7 @@ suite('NotebookConcatDocument', function () { let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.apiNotebook, undefined); // UPDATE 1 - extHostNotebooks.$acceptModelChanged(notebookUri, { + extHostNotebookDocuments.$acceptModelChanged(notebookUri, { versionId: notebook.apiNotebook.version + 1, rawEvents: [ { @@ -340,7 +345,7 @@ suite('NotebookConcatDocument', function () { test('selector', function () { - extHostNotebooks.$acceptModelChanged(notebookUri, { + extHostNotebookDocuments.$acceptModelChanged(notebookUri, { versionId: notebook.apiNotebook.version + 1, rawEvents: [ { @@ -374,7 +379,7 @@ suite('NotebookConcatDocument', function () { assertLines(fooLangDoc, 'fooLang-document'); assertLines(barLangDoc, 'barLang-document'); - extHostNotebooks.$acceptModelChanged(notebookUri, { + extHostNotebookDocuments.$acceptModelChanged(notebookUri, { versionId: notebook.apiNotebook.version + 1, rawEvents: [ { @@ -412,7 +417,7 @@ suite('NotebookConcatDocument', function () { test('offsetAt(position) <-> positionAt(offset)', function () { - extHostNotebooks.$acceptModelChanged(notebookUri, { + extHostNotebookDocuments.$acceptModelChanged(notebookUri, { versionId: notebook.apiNotebook.version + 1, rawEvents: [ { @@ -469,7 +474,7 @@ suite('NotebookConcatDocument', function () { test('locationAt(position) <-> positionAt(location)', function () { - extHostNotebooks.$acceptModelChanged(notebookUri, { + extHostNotebookDocuments.$acceptModelChanged(notebookUri, { versionId: notebook.apiNotebook.version + 1, rawEvents: [ { @@ -510,7 +515,7 @@ suite('NotebookConcatDocument', function () { test('getText(range)', function () { - extHostNotebooks.$acceptModelChanged(notebookUri, { + extHostNotebookDocuments.$acceptModelChanged(notebookUri, { versionId: notebook.apiNotebook.version + 1, rawEvents: [ { @@ -548,7 +553,7 @@ suite('NotebookConcatDocument', function () { test('validateRange/Position', function () { - extHostNotebooks.$acceptModelChanged(notebookUri, { + extHostNotebookDocuments.$acceptModelChanged(notebookUri, { versionId: notebook.apiNotebook.version + 1, rawEvents: [ { diff --git a/src/vs/workbench/test/browser/api/extHostNotebookKernel2.test.ts b/src/vs/workbench/test/browser/api/extHostNotebookKernel2.test.ts index 1af0ca885c..f45abedf59 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebookKernel2.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebookKernel2.test.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; @@ -8,18 +8,39 @@ import { TestRPCProtocol } from 'vs/workbench/test/browser/api/testRPCProtocol'; import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { mock } from 'vs/workbench/test/common/workbenchTestServices'; -import { INotebookKernelDto2, MainContext, MainThreadCommandsShape, MainThreadNotebookKernelsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { INotebookKernelDto2, MainContext, MainThreadCommandsShape, MainThreadNotebookDocumentsShape, MainThreadNotebookKernelsShape, MainThreadNotebookShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostNotebookKernels } from 'vs/workbench/api/common/extHostNotebookKernels'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; +import { ExtHostNotebookDocument } from 'vs/workbench/api/common/extHostNotebookDocument'; +import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { CellKind, CellUri, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNotebookDocuments'; suite('NotebookKernel', function () { let rpcProtocol: TestRPCProtocol; let extHostNotebookKernels: ExtHostNotebookKernels; + let notebook: ExtHostNotebookDocument; + let extHostDocumentsAndEditors: ExtHostDocumentsAndEditors; + let extHostDocuments: ExtHostDocuments; + let extHostNotebooks: ExtHostNotebookController; + let extHostNotebookDocuments: ExtHostNotebookDocuments; + const notebookUri = URI.parse('test:///notebook.file'); const kernelData = new Map(); + const disposables = new DisposableStore(); + teardown(function () { + disposables.clear(); + }); setup(async function () { kernelData.clear(); @@ -40,11 +61,67 @@ suite('NotebookKernel', function () { kernelData.set(handle, { ...kernelData.get(handle)!, ...data, }); } }); + rpcProtocol.set(MainContext.MainThreadNotebookDocuments, new class extends mock() { + override async $applyEdits() { } + }); + rpcProtocol.set(MainContext.MainThreadNotebook, new class extends mock() { + override async $registerNotebookProvider() { } + override async $unregisterNotebookProvider() { } + }); + extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()); + extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); + const extHostStoragePaths = new class extends mock() { + override workspaceValue() { + return URI.from({ scheme: 'test', path: generateUuid() }); + } + }; + extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors, extHostDocuments, extHostStoragePaths); + + extHostNotebookDocuments = new ExtHostNotebookDocuments(new NullLogService(), extHostNotebooks); + + extHostNotebooks.$acceptDocumentAndEditorsDelta({ + addedDocuments: [{ + uri: notebookUri, + viewType: 'test', + versionId: 0, + cells: [{ + handle: 0, + uri: CellUri.generate(notebookUri, 0), + source: ['### Heading'], + eol: '\n', + language: 'markdown', + cellKind: CellKind.Markup, + outputs: [], + }, { + handle: 1, + uri: CellUri.generate(notebookUri, 1), + source: ['console.log("aaa")', 'console.log("bbb")'], + eol: '\n', + language: 'javascript', + cellKind: CellKind.Code, + outputs: [], + }], + }], + addedEditors: [{ + documentUri: notebookUri, + id: '_notebook_editor_0', + selections: [{ start: 0, end: 1 }], + visibleRanges: [] + }] + }); + extHostNotebooks.$acceptDocumentAndEditorsDelta({ newActiveEditor: '_notebook_editor_0' }); + + notebook = extHostNotebooks.notebookDocuments[0]!; + + disposables.add(notebook); + disposables.add(extHostDocuments); + extHostNotebookKernels = new ExtHostNotebookKernels( rpcProtocol, new class extends mock() { }, - new class extends mock() { } + extHostNotebooks, + new NullLogService() ); }); @@ -53,12 +130,12 @@ suite('NotebookKernel', function () { const kernel = extHostNotebookKernels.createNotebookController(nullExtensionDescription, 'foo', '*', 'Foo'); assert.throws(() => (kernel).id = 'dd'); - assert.throws(() => (kernel).viewType = 'dd'); + assert.throws(() => (kernel).notebookType = 'dd'); assert.ok(kernel); assert.strictEqual(kernel.id, 'foo'); assert.strictEqual(kernel.label, 'Foo'); - assert.strictEqual(kernel.viewType, '*'); + assert.strictEqual(kernel.notebookType, '*'); await rpcProtocol.sync(); assert.strictEqual(kernelData.size, 1); @@ -67,7 +144,7 @@ suite('NotebookKernel', function () { assert.strictEqual(first.id, 'nullExtensionDescription/foo'); assert.strictEqual(ExtensionIdentifier.equals(first.extensionId, nullExtensionDescription.identifier), true); assert.strictEqual(first.label, 'Foo'); - assert.strictEqual(first.viewType, '*'); + assert.strictEqual(first.notebookType, '*'); kernel.dispose(); await rpcProtocol.sync(); @@ -93,4 +170,69 @@ suite('NotebookKernel', function () { assert.strictEqual(first.id, 'nullExtensionDescription/foo'); assert.strictEqual(first.label, 'Far'); }); + + test('execute - simple createNotebookCellExecution', function () { + const kernel = extHostNotebookKernels.createNotebookController(nullExtensionDescription, 'foo', '*', 'Foo'); + + extHostNotebookKernels.$acceptNotebookAssociation(0, notebook.uri, true); + + const cell1 = notebook.apiNotebook.cellAt(0); + const task = kernel.createNotebookCellExecution(cell1); + task.start(); + task.end(undefined); + }); + + test('createNotebookCellExecution, must be selected/associated', function () { + const kernel = extHostNotebookKernels.createNotebookController(nullExtensionDescription, 'foo', '*', 'Foo'); + assert.throws(() => { + kernel.createNotebookCellExecution(notebook.apiNotebook.cellAt(0)); + }); + + extHostNotebookKernels.$acceptNotebookAssociation(0, notebook.uri, true); + kernel.createNotebookCellExecution(notebook.apiNotebook.cellAt(0)); + }); + + test('createNotebookCellExecution, cell must be alive', function () { + const kernel = extHostNotebookKernels.createNotebookController(nullExtensionDescription, 'foo', '*', 'Foo'); + + const cell1 = notebook.apiNotebook.cellAt(0); + + extHostNotebookKernels.$acceptNotebookAssociation(0, notebook.uri, true); + extHostNotebookDocuments.$acceptModelChanged(notebook.uri, { + versionId: 12, + rawEvents: [{ + kind: NotebookCellsChangeType.ModelChange, + changes: [[0, notebook.apiNotebook.cellCount, []]] + }] + }, true); + + assert.strictEqual(cell1.index, -1); + + assert.throws(() => { + kernel.createNotebookCellExecution(cell1); + }); + }); + + test('interrupt handler, cancellation', async function () { + + let interruptCallCount = 0; + let tokenCancelCount = 0; + + const kernel = extHostNotebookKernels.createNotebookController(nullExtensionDescription, 'foo', '*', 'Foo'); + kernel.interruptHandler = () => { interruptCallCount += 1; }; + extHostNotebookKernels.$acceptNotebookAssociation(0, notebook.uri, true); + + const cell1 = notebook.apiNotebook.cellAt(0); + + const task = kernel.createNotebookCellExecution(cell1); + task.token.onCancellationRequested(() => tokenCancelCount += 1); + + await extHostNotebookKernels.$cancelCells(0, notebook.uri, [0]); + assert.strictEqual(interruptCallCount, 1); + assert.strictEqual(tokenCancelCount, 0); + + await extHostNotebookKernels.$cancelCells(0, notebook.uri, [0]); + assert.strictEqual(interruptCallCount, 2); + assert.strictEqual(tokenCancelCount, 0); + }); }); diff --git a/src/vs/workbench/test/browser/api/extHostTreeViews.test.ts b/src/vs/workbench/test/browser/api/extHostTreeViews.test.ts index f5e2eace95..e8590463e9 100644 --- a/src/vs/workbench/test/browser/api/extHostTreeViews.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTreeViews.test.ts @@ -15,12 +15,12 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { MainThreadCommands } from 'vs/workbench/api/browser/mainThreadCommands'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { mock } from 'vs/base/test/common/mock'; -import { TreeItemCollapsibleState, ITreeItem } from 'vs/workbench/common/views'; +import { TreeItemCollapsibleState, ITreeItem, IRevealOptions } from 'vs/workbench/common/views'; import { NullLogService } from 'vs/platform/log/common/log'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import type { IDisposable } from 'vs/base/common/lifecycle'; -suite.skip('ExtHostTreeView', function () { +suite.skip('ExtHostTreeView', function () { // {{SQL CARBON EDIT}} Skip suite class RecordingShape extends mock() { @@ -35,7 +35,7 @@ suite.skip('ExtHostTreeView', function () { }); } - override $reveal(): Promise { + override $reveal(treeViewId: string, itemInfo: { item: ITreeItem, parentChain: ITreeItem[] } | undefined, options: IRevealOptions): Promise { return Promise.resolve(); } @@ -515,8 +515,8 @@ suite.skip('ExtHostTreeView', function () { .then(() => { assert.ok(revealTarget.calledOnce); assert.deepStrictEqual('treeDataProvider', revealTarget.args[0][0]); - assert.deepStrictEqual(expected.item, removeUnsetKeys(revealTarget.args[0][1].item)); - assert.deepStrictEqual(expected.parentChain, (>(revealTarget.args[0][1].parentChain)).map(arg => removeUnsetKeys(arg))); + assert.deepStrictEqual(expected.item, removeUnsetKeys(revealTarget.args[0][1]!.item)); + assert.deepStrictEqual(expected.parentChain, (>(revealTarget.args[0][1]!.parentChain)).map(arg => removeUnsetKeys(arg))); assert.deepStrictEqual({ select: true, focus: false, expand: false }, revealTarget.args[0][2]); }); }); @@ -534,8 +534,8 @@ suite.skip('ExtHostTreeView', function () { .then(() => { assert.ok(revealTarget.calledOnce); assert.deepStrictEqual('treeDataProvider', revealTarget.args[0][0]); - assert.deepStrictEqual(expected.item, removeUnsetKeys(revealTarget.args[0][1].item)); - assert.deepStrictEqual(expected.parentChain, (>(revealTarget.args[0][1].parentChain)).map(arg => removeUnsetKeys(arg))); + assert.deepStrictEqual(expected.item, removeUnsetKeys(revealTarget.args[0][1]!.item)); + assert.deepStrictEqual(expected.parentChain, (>(revealTarget.args[0][1]!.parentChain)).map(arg => removeUnsetKeys(arg))); assert.deepStrictEqual({ select: true, focus: false, expand: false }, revealTarget.args[0][2]); })); }); @@ -561,8 +561,8 @@ suite.skip('ExtHostTreeView', function () { .then(() => { assert.ok(revealTarget.calledOnce); assert.deepStrictEqual('treeDataProvider', revealTarget.args[0][0]); - assert.deepStrictEqual(expected.item, removeUnsetKeys(revealTarget.args[0][1].item)); - assert.deepStrictEqual(expected.parentChain, (>(revealTarget.args[0][1].parentChain)).map(arg => removeUnsetKeys(arg))); + assert.deepStrictEqual(expected.item, removeUnsetKeys(revealTarget.args[0][1]!.item)); + assert.deepStrictEqual(expected.parentChain, (>(revealTarget.args[0][1]!.parentChain)).map(arg => removeUnsetKeys(arg))); assert.deepStrictEqual({ select: false, focus: false, expand: false }, revealTarget.args[0][2]); }); }); @@ -592,8 +592,8 @@ suite.skip('ExtHostTreeView', function () { .then(() => { assert.ok(revealTarget.calledOnce); assert.deepStrictEqual('treeDataProvider', revealTarget.args[0][0]); - assert.deepStrictEqual(expected.item, removeUnsetKeys(revealTarget.args[0][1].item)); - assert.deepStrictEqual(expected.parentChain, (>(revealTarget.args[0][1].parentChain)).map(arg => removeUnsetKeys(arg))); + assert.deepStrictEqual(expected.item, removeUnsetKeys(revealTarget.args[0][1]!.item)); + assert.deepStrictEqual(expected.parentChain, (>(revealTarget.args[0][1]!.parentChain)).map(arg => removeUnsetKeys(arg))); assert.deepStrictEqual({ select: true, focus: false, expand: false }, revealTarget.args[0][2]); }); }); @@ -633,8 +633,8 @@ suite.skip('ExtHostTreeView', function () { .then(() => { assert.ok(revealTarget.calledOnce); assert.deepStrictEqual('treeDataProvider', revealTarget.args[0][0]); - assert.deepStrictEqual({ handle: '0/0:b/0:bc', label: { label: 'bc' }, collapsibleState: TreeItemCollapsibleState.None, parentHandle: '0/0:b' }, removeUnsetKeys(revealTarget.args[0][1].item)); - assert.deepStrictEqual([{ handle: '0/0:b', label: { label: 'b' }, collapsibleState: TreeItemCollapsibleState.Collapsed }], (>revealTarget.args[0][1].parentChain).map(arg => removeUnsetKeys(arg))); + assert.deepStrictEqual({ handle: '0/0:b/0:bc', label: { label: 'bc' }, collapsibleState: TreeItemCollapsibleState.None, parentHandle: '0/0:b' }, removeUnsetKeys(revealTarget.args[0][1]!.item)); + assert.deepStrictEqual([{ handle: '0/0:b', label: { label: 'b' }, collapsibleState: TreeItemCollapsibleState.Collapsed }], (>revealTarget.args[0][1]!.parentChain).map(arg => removeUnsetKeys(arg))); assert.deepStrictEqual({ select: true, focus: false, expand: false }, revealTarget.args[0][2]); }); }); diff --git a/src/vs/workbench/test/browser/api/extHostTypeConverter.test.ts b/src/vs/workbench/test/browser/api/extHostTypeConverter.test.ts index d90348894f..b50403f16a 100644 --- a/src/vs/workbench/test/browser/api/extHostTypeConverter.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTypeConverter.test.ts @@ -5,7 +5,8 @@ import * as assert from 'assert'; -import { MarkdownString } from 'vs/workbench/api/common/extHostTypeConverters'; +import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; +import { MarkdownString, NotebookCellOutputItem, NotebookData } from 'vs/workbench/api/common/extHostTypeConverters'; import { isEmptyObject } from 'vs/base/common/types'; import { forEach } from 'vs/base/common/collections'; import { LogLevel as _MainLogLevel } from 'vs/platform/log/common/log'; @@ -81,4 +82,33 @@ suite('ExtHostTypeConverter', function () { } }); }); + + test('Notebook metadata is ignored when using Notebook Serializer #125716', function () { + + const d = new extHostTypes.NotebookData([]); + d.cells.push(new extHostTypes.NotebookCellData(extHostTypes.NotebookCellKind.Code, 'hello', 'fooLang')); + d.metadata = { custom: { foo: 'bar', bar: 123 } }; + + const dto = NotebookData.from(d); + + assert.strictEqual(dto.cells.length, 1); + assert.strictEqual(dto.cells[0].language, 'fooLang'); + assert.strictEqual(dto.cells[0].source, 'hello'); + assert.deepStrictEqual(dto.metadata, d.metadata); + }); + + test('NotebookCellOutputItem', function () { + + const item = extHostTypes.NotebookCellOutputItem.text('Hello', 'foo/bar'); + + const dto = NotebookCellOutputItem.from(item); + + assert.strictEqual(dto.mime, 'foo/bar'); + assert.deepStrictEqual(dto.valueBytes, Array.from(new TextEncoder().encode('Hello'))); + + const item2 = NotebookCellOutputItem.to(dto); + + assert.strictEqual(item2.mime, item.mime); + assert.deepStrictEqual(item2.data, item.data); + }); }); diff --git a/src/vs/workbench/test/browser/api/extHostTypes.test.ts b/src/vs/workbench/test/browser/api/extHostTypes.test.ts index 69b0250fb9..16306f0e87 100644 --- a/src/vs/workbench/test/browser/api/extHostTypes.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTypes.test.ts @@ -8,7 +8,6 @@ import { URI } from 'vs/base/common/uri'; import * as types from 'vs/workbench/api/common/extHostTypes'; import { isWindows } from 'vs/base/common/platform'; import { assertType } from 'vs/base/common/types'; -import { notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; function assertToJSON(a: any, expected: any) { const raw = JSON.stringify(a); @@ -649,57 +648,46 @@ suite('ExtHostTypes', function () { assert.deepStrictEqual(md.value, '\n```html\n\n```\n'); }); - test('NotebookMetadata - defaults', function () { - const obj = new types.NotebookDocumentMetadata(); - assert.strictEqual(obj.trusted, notebookDocumentMetadataDefaults.trusted); - }); + test('NotebookCellOutputItem - factories', function () { - test('NotebookMetadata - with', function () { - const obj = new types.NotebookDocumentMetadata(); - const newObj = obj.with({ trusted: false }); - assert.ok(obj !== newObj); - const sameObj = newObj.with({ trusted: false }); - assert.ok(newObj === sameObj); - assert.strictEqual(obj.trusted, true); - assert.strictEqual(newObj.trusted, false); - }); + assert.throws(() => { + // invalid mime type + new types.NotebookCellOutputItem(new Uint8Array(), 'invalid'); + }); - test('NotebookMetadata - with custom', function () { - const obj = new types.NotebookDocumentMetadata(); - const newObj = obj.with({ trusted: false, mycustom: { display: 'hello' } }); - assert.ok(obj !== newObj); - const sameObj = newObj.with({ trusted: false }); - assert.ok(newObj === sameObj); - assert.strictEqual(obj.trusted, true); - assert.strictEqual(newObj.trusted, false); - assert.deepStrictEqual(newObj.mycustom, { display: 'hello' }); - }); + // --- err - test('NotebookCellMetadata - with', function () { - const obj = new types.NotebookCellMetadata(true, true); + let item = types.NotebookCellOutputItem.error(new Error()); + assert.strictEqual(item.mime, 'application/vnd.code.notebook.error'); + item = types.NotebookCellOutputItem.error({ name: 'Hello' }); + assert.strictEqual(item.mime, 'application/vnd.code.notebook.error'); - const newObj = obj.with({ inputCollapsed: false }); - assert.ok(obj !== newObj); - assert.strictEqual(obj.inputCollapsed, true); - assert.strictEqual(obj.custom, undefined); + // --- JSON - assert.strictEqual(newObj.inputCollapsed, false); - assert.strictEqual(newObj.custom, undefined); - }); + item = types.NotebookCellOutputItem.json(1); + assert.strictEqual(item.mime, 'application/json'); + assert.deepStrictEqual(item.data, new TextEncoder().encode(JSON.stringify(1))); - test('NotebookCellMetadata - with custom', function () { - const obj = new types.NotebookCellMetadata(true, true); - const newObj = obj.with({ inputCollapsed: false, custom: { display: 'hello' } }); - assert.ok(obj !== newObj); - const sameObj = newObj.with({ inputCollapsed: false }); - assert.ok(newObj === sameObj); - assert.strictEqual(obj.inputCollapsed, true); - assert.strictEqual(newObj.inputCollapsed, false); - assert.deepStrictEqual(newObj.custom, { display: 'hello' }); + item = types.NotebookCellOutputItem.json(1, 'foo/bar'); + assert.strictEqual(item.mime, 'foo/bar'); + assert.deepStrictEqual(item.data, new TextEncoder().encode(JSON.stringify(1))); - const newCustom = newObj.with({ anotherCustom: { display: 'hello2' } }); - assert.strictEqual(newCustom.inputCollapsed, false); - assert.deepStrictEqual(newCustom.mycustom, undefined); - assert.deepStrictEqual(newCustom.anotherCustom, { display: 'hello2' }); + item = types.NotebookCellOutputItem.json(true); + assert.strictEqual(item.mime, 'application/json'); + assert.deepStrictEqual(item.data, new TextEncoder().encode(JSON.stringify(true))); + + item = types.NotebookCellOutputItem.json([true, 1, 'ddd']); + assert.strictEqual(item.mime, 'application/json'); + assert.deepStrictEqual(item.data, new TextEncoder().encode(JSON.stringify([true, 1, 'ddd'], undefined, '\t'))); + + // --- text + + item = types.NotebookCellOutputItem.text('Hęłlö'); + assert.strictEqual(item.mime, 'text/plain'); + assert.deepStrictEqual(item.data, new TextEncoder().encode('Hęłlö')); + + item = types.NotebookCellOutputItem.text('Hęłlö', 'foo/bar'); + assert.strictEqual(item.mime, 'foo/bar'); + assert.deepStrictEqual(item.data, new TextEncoder().encode('Hęłlö')); }); }); diff --git a/src/vs/workbench/test/browser/api/extHostWebview.test.ts b/src/vs/workbench/test/browser/api/extHostWebview.test.ts index 72b486341b..3debfb8053 100644 --- a/src/vs/workbench/test/browser/api/extHostWebview.test.ts +++ b/src/vs/workbench/test/browser/api/extHostWebview.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; @@ -14,6 +15,7 @@ import { NullApiDeprecationService } from 'vs/workbench/api/common/extHostApiDep import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview'; import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels'; +import { webviewResourceBaseHost } from 'vs/workbench/api/common/shared/webview'; import { EditorGroupColumn } from 'vs/workbench/common/editor'; import type * as vscode from 'vscode'; import { SingleProxyRPCProtocol } from './testRPCProtocol'; @@ -30,11 +32,7 @@ suite('ExtHostWebview', () => { test('Cannot register multiple serializers for the same view type', async () => { const viewType = 'view.type'; - const extHostWebviews = new ExtHostWebviews(rpcProtocol!, { - webviewCspSource: '', - webviewResourceRoot: '', - isExtensionDevelopmentDebug: false, - }, undefined, new NullLogService(), NullApiDeprecationService); + const extHostWebviews = new ExtHostWebviews(rpcProtocol!, { remote: { authority: undefined, isRemote: false } }, undefined, new NullLogService(), NullApiDeprecationService); const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); @@ -78,95 +76,71 @@ suite('ExtHostWebview', () => { assert.strictEqual(lastInvokedDeserializer, serializerB); }); - test('asWebviewUri for desktop vscode-resource scheme', () => { - const extHostWebviews = new ExtHostWebviews(rpcProtocol!, { - webviewCspSource: '', - webviewResourceRoot: 'vscode-resource://{{resource}}', - isExtensionDevelopmentDebug: false, - }, undefined, new NullLogService(), NullApiDeprecationService); - - const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); - - const webview = extHostWebviewPanels.createWebviewPanel({} as any, 'type', 'title', 1, {}); + test('asWebviewUri for local file paths', () => { + const webview = createWebview(rpcProtocol, /* remoteAuthority */undefined); assert.strictEqual( - webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString(), - 'vscode-resource://file///Users/codey/file.html', + (webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString()), + `https://file%2B.vscode-resource.${webviewResourceBaseHost}/Users/codey/file.html`, 'Unix basic' ); assert.strictEqual( - webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html#frag')).toString(), - 'vscode-resource://file///Users/codey/file.html#frag', + (webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html#frag')).toString()), + `https://file%2B.vscode-resource.${webviewResourceBaseHost}/Users/codey/file.html#frag`, 'Unix should preserve fragment' ); assert.strictEqual( - webview.webview.asWebviewUri(URI.parse('file:///Users/codey/f%20ile.html')).toString(), - 'vscode-resource://file///Users/codey/f%20ile.html', + (webview.webview.asWebviewUri(URI.parse('file:///Users/codey/f%20ile.html')).toString()), + `https://file%2B.vscode-resource.${webviewResourceBaseHost}/Users/codey/f%20ile.html`, 'Unix with encoding' ); assert.strictEqual( - webview.webview.asWebviewUri(URI.parse('file://localhost/Users/codey/file.html')).toString(), - 'vscode-resource://file//localhost/Users/codey/file.html', + (webview.webview.asWebviewUri(URI.parse('file://localhost/Users/codey/file.html')).toString()), + `https://file%2Blocalhost.vscode-resource.${webviewResourceBaseHost}/Users/codey/file.html`, 'Unix should preserve authority' ); assert.strictEqual( - webview.webview.asWebviewUri(URI.parse('file:///c:/codey/file.txt')).toString(), - 'vscode-resource://file///c%3A/codey/file.txt', + (webview.webview.asWebviewUri(URI.parse('file:///c:/codey/file.txt')).toString()), + `https://file%2B.vscode-resource.${webviewResourceBaseHost}/c%3A/codey/file.txt`, 'Windows C drive' ); }); - test('asWebviewUri for web endpoint', () => { - const extHostWebviews = new ExtHostWebviews(rpcProtocol!, { - webviewCspSource: '', - webviewResourceRoot: `https://{{uuid}}.webview.contoso.com/commit/{{resource}}`, - isExtensionDevelopmentDebug: false, - }, undefined, new NullLogService(), NullApiDeprecationService); - - const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); - - const webview = extHostWebviewPanels.createWebviewPanel({} as any, 'type', 'title', 1, {}); - - function stripEndpointUuid(input: string) { - return input.replace(/^https:\/\/[^\.]+?\./, ''); - } + test('asWebviewUri for remote file paths', () => { + const webview = createWebview(rpcProtocol, /* remoteAuthority */ 'remote'); assert.strictEqual( - stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString()), - 'webview.contoso.com/commit/file///Users/codey/file.html', + (webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString()), + `https://vscode-remote%2Bremote.vscode-resource.${webviewResourceBaseHost}/Users/codey/file.html`, 'Unix basic' ); - - assert.strictEqual( - stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html#frag')).toString()), - 'webview.contoso.com/commit/file///Users/codey/file.html#frag', - 'Unix should preserve fragment' - ); - - assert.strictEqual( - stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/f%20ile.html')).toString()), - 'webview.contoso.com/commit/file///Users/codey/f%20ile.html', - 'Unix with encoding' - ); - - assert.strictEqual( - stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file://localhost/Users/codey/file.html')).toString()), - 'webview.contoso.com/commit/file//localhost/Users/codey/file.html', - 'Unix should preserve authority' - ); - - assert.strictEqual( - stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///c:/codey/file.txt')).toString()), - 'webview.contoso.com/commit/file///c%3A/codey/file.txt', - 'Windows C drive' - ); }); }); +function createWebview(rpcProtocol: (IExtHostRpcService & IExtHostContext) | undefined, remoteAuthority: string | undefined) { + const extHostWebviews = new ExtHostWebviews(rpcProtocol!, { + remote: { + authority: remoteAuthority, + isRemote: !!remoteAuthority, + }, + }, undefined, new NullLogService(), NullApiDeprecationService); + + const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); + + const webview = extHostWebviewPanels.createWebviewPanel({ + extensionLocation: URI.from({ + scheme: remoteAuthority ? Schemas.vscodeRemote : Schemas.file, + authority: remoteAuthority, + path: '/ext/path', + }) + } as IExtensionDescription, 'type', 'title', 1, {}); + return webview; +} + function createNoopMainThreadWebviews() { return new class extends mock() { diff --git a/src/vs/workbench/test/browser/parts/editor/diffEditorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/diffEditorInput.test.ts new file mode 100644 index 0000000000..f8546d761f --- /dev/null +++ b/src/vs/workbench/test/browser/parts/editor/diffEditorInput.test.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; + +suite('Diff editor input', () => { + + class MyEditorInput extends EditorInput { + readonly resource = undefined; + + override get typeId(): string { return 'myEditorInput'; } + override resolve(): any { return null; } + } + + test('basics', () => { + const instantiationService = workbenchInstantiationService(); + + let counter = 0; + let input = new MyEditorInput(); + input.onWillDispose(() => { + assert(true); + counter++; + }); + + let otherInput = new MyEditorInput(); + otherInput.onWillDispose(() => { + assert(true); + counter++; + }); + + let diffInput = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined); + + assert.strictEqual(diffInput.originalInput, input); + assert.strictEqual(diffInput.modifiedInput, otherInput); + assert(diffInput.matches(diffInput)); + assert(!diffInput.matches(otherInput)); + assert(!diffInput.matches(null)); + + diffInput.dispose(); + assert.strictEqual(counter, 0); + }); + + test('disposes when input inside disposes', function () { + const instantiationService = workbenchInstantiationService(); + + let counter = 0; + let input = new MyEditorInput(); + let otherInput = new MyEditorInput(); + + let diffInput = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined); + diffInput.onWillDispose(() => { + counter++; + assert(true); + }); + + input.dispose(); + + input = new MyEditorInput(); + otherInput = new MyEditorInput(); + + let diffInput2 = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined); + diffInput2.onWillDispose(() => { + counter++; + assert(true); + }); + + otherInput.dispose(); + assert.strictEqual(counter, 2); + }); +}); diff --git a/src/vs/workbench/test/browser/parts/editor/editor.test.ts b/src/vs/workbench/test/browser/parts/editor/editor.test.ts index 4410e5f18a..e96ad7afa7 100644 --- a/src/vs/workbench/test/browser/parts/editor/editor.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editor.test.ts @@ -4,15 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { EditorResourceAccessor, SideBySideEditor, IEditorInputWithPreferredResource } from 'vs/workbench/common/editor'; +import { EditorResourceAccessor, SideBySideEditor, IEditorInputWithPreferredResource, EditorInputCapabilities, isEditorIdentifier } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { workbenchInstantiationService, TestServiceAccessor, TestEditorInput } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, TestServiceAccessor, TestEditorInput, registerTestEditor, registerTestFileEditor, registerTestResourceEditor, TestFileEditorInput, createEditorPart, registerTestSideBySideEditor } from 'vs/workbench/test/browser/workbenchTestServices'; import { Schemas } from 'vs/base/common/network'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { toResource } from 'vs/base/test/common/utils'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { whenEditorClosed } from 'vs/workbench/browser/editor'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; -suite('Workbench editor', () => { +suite('Workbench editor utils', () => { class TestEditorInputWithPreferredResource extends TestEditorInput implements IEditorInputWithPreferredResource { @@ -21,16 +29,93 @@ suite('Workbench editor', () => { } } + const disposables = new DisposableStore(); + + const TEST_EDITOR_ID = 'MyTestEditorForEditors'; + let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; + async function createServices(): Promise { + const instantiationService = workbenchInstantiationService(); + + const part = await createEditorPart(instantiationService, disposables); + + instantiationService.stub(IEditorGroupsService, part); + + const editorService = instantiationService.createInstance(EditorService); + instantiationService.stub(IEditorService, editorService); + + return instantiationService.createInstance(TestServiceAccessor); + } + setup(() => { instantiationService = workbenchInstantiationService(); accessor = instantiationService.createInstance(TestServiceAccessor); + + disposables.add(registerTestFileEditor()); + disposables.add(registerTestSideBySideEditor()); + disposables.add(registerTestResourceEditor()); + disposables.add(registerTestEditor(TEST_EDITOR_ID, [new SyncDescriptor(TestFileEditorInput)])); }); teardown(() => { accessor.untitledTextEditorService.dispose(); + + disposables.clear(); + }); + + test('EditorInputCapabilities', () => { + const testInput1 = new TestFileEditorInput(URI.file('resource1'), 'testTypeId'); + const testInput2 = new TestFileEditorInput(URI.file('resource2'), 'testTypeId'); + + testInput1.capabilities = EditorInputCapabilities.None; + assert.strictEqual(testInput1.hasCapability(EditorInputCapabilities.None), true); + assert.strictEqual(testInput1.hasCapability(EditorInputCapabilities.Readonly), false); + assert.strictEqual(testInput1.hasCapability(EditorInputCapabilities.Untitled), false); + assert.strictEqual(testInput1.hasCapability(EditorInputCapabilities.RequiresTrust), false); + assert.strictEqual(testInput1.hasCapability(EditorInputCapabilities.Singleton), false); + + testInput1.capabilities |= EditorInputCapabilities.Readonly; + assert.strictEqual(testInput1.hasCapability(EditorInputCapabilities.Readonly), true); + assert.strictEqual(testInput1.hasCapability(EditorInputCapabilities.None), false); + assert.strictEqual(testInput1.hasCapability(EditorInputCapabilities.Untitled), false); + assert.strictEqual(testInput1.hasCapability(EditorInputCapabilities.RequiresTrust), false); + assert.strictEqual(testInput1.hasCapability(EditorInputCapabilities.Singleton), false); + + testInput1.capabilities = EditorInputCapabilities.None; + testInput2.capabilities = EditorInputCapabilities.None; + + const sideBySideInput = new SideBySideEditorInput('name', undefined, testInput1, testInput2); + assert.strictEqual(sideBySideInput.hasCapability(EditorInputCapabilities.None), true); + assert.strictEqual(sideBySideInput.hasCapability(EditorInputCapabilities.Readonly), false); + assert.strictEqual(sideBySideInput.hasCapability(EditorInputCapabilities.Untitled), false); + assert.strictEqual(sideBySideInput.hasCapability(EditorInputCapabilities.RequiresTrust), false); + assert.strictEqual(sideBySideInput.hasCapability(EditorInputCapabilities.Singleton), false); + + testInput1.capabilities |= EditorInputCapabilities.Readonly; + assert.strictEqual(sideBySideInput.hasCapability(EditorInputCapabilities.Readonly), false); + + testInput2.capabilities |= EditorInputCapabilities.Readonly; + assert.strictEqual(sideBySideInput.hasCapability(EditorInputCapabilities.Readonly), true); + + testInput1.capabilities |= EditorInputCapabilities.Untitled; + assert.strictEqual(sideBySideInput.hasCapability(EditorInputCapabilities.Untitled), false); + + testInput2.capabilities |= EditorInputCapabilities.Untitled; + assert.strictEqual(sideBySideInput.hasCapability(EditorInputCapabilities.Untitled), true); + + testInput1.capabilities |= EditorInputCapabilities.RequiresTrust; + assert.strictEqual(sideBySideInput.hasCapability(EditorInputCapabilities.RequiresTrust), true); + + testInput2.capabilities |= EditorInputCapabilities.RequiresTrust; + assert.strictEqual(sideBySideInput.hasCapability(EditorInputCapabilities.RequiresTrust), true); + + testInput1.capabilities |= EditorInputCapabilities.Singleton; + assert.strictEqual(sideBySideInput.hasCapability(EditorInputCapabilities.Singleton), true); + + testInput2.capabilities |= EditorInputCapabilities.Singleton; + assert.strictEqual(sideBySideInput.hasCapability(EditorInputCapabilities.Singleton), true); }); test('EditorResourceAccessor', () => { @@ -123,4 +208,57 @@ suite('Workbench editor', () => { assert.strictEqual(EditorResourceAccessor.getCanonicalUri(fileWithPreferredResource)?.toString(), resource.toString()); assert.strictEqual(EditorResourceAccessor.getOriginalUri(fileWithPreferredResource)?.toString(), preferredResource.toString()); }); + + test('isEditorIdentifier', () => { + assert.strictEqual(isEditorIdentifier(undefined), false); + assert.strictEqual(isEditorIdentifier('undefined'), false); + + const testInput1 = new TestFileEditorInput(URI.file('resource1'), 'testTypeId'); + assert.strictEqual(isEditorIdentifier(testInput1), false); + assert.strictEqual(isEditorIdentifier({ editor: testInput1, groupId: 3 }), true); + }); + + test('whenEditorClosed (single editor)', async function () { + return testWhenEditorClosed(false, false, toResource.call(this, '/path/index.txt')); + }); + + test('whenEditorClosed (multiple editor)', async function () { + return testWhenEditorClosed(false, false, toResource.call(this, '/path/index.txt'), toResource.call(this, '/test.html')); + }); + + test('whenEditorClosed (single editor, diff editor)', async function () { + return testWhenEditorClosed(true, false, toResource.call(this, '/path/index.txt')); + }); + + test('whenEditorClosed (multiple editor, diff editor)', async function () { + return testWhenEditorClosed(true, false, toResource.call(this, '/path/index.txt'), toResource.call(this, '/test.html')); + }); + + test('whenEditorClosed (single custom editor)', async function () { + return testWhenEditorClosed(false, true, toResource.call(this, '/path/index.txt')); + }); + + test('whenEditorClosed (multiple custom editor)', async function () { + return testWhenEditorClosed(false, true, toResource.call(this, '/path/index.txt'), toResource.call(this, '/test.html')); + }); + + async function testWhenEditorClosed(sideBySide: boolean, custom: boolean, ...resources: URI[]): Promise { + const accessor = await createServices(); + + for (const resource of resources) { + if (custom) { + await accessor.editorService.openEditor(new TestFileEditorInput(resource, 'testTypeId'), { pinned: true }); + } else if (sideBySide) { + await accessor.editorService.openEditor(new SideBySideEditorInput('testSideBySideEditor', undefined, new TestFileEditorInput(resource, 'testTypeId'), new TestFileEditorInput(resource, 'testTypeId')), { pinned: true }); + } else { + await accessor.editorService.openEditor({ resource, options: { pinned: true } }); + } + } + + const closedPromise = accessor.instantitionService.invokeFunction(accessor => whenEditorClosed(accessor, resources)); + + accessor.editorGroupService.activeGroup.closeAllEditors(); + + await closedPromise; + } }); diff --git a/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts index 3ae3b86c05..dbab27af6f 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts @@ -6,13 +6,13 @@ import * as assert from 'assert'; import { TextDiffEditorModel } from 'vs/workbench/common/editor/textDiffEditorModel'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; -import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; +import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { URI } from 'vs/base/common/uri'; import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; import { ITextModel } from 'vs/editor/common/model'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -suite('Workbench editor model', () => { +suite('TextDiffEditorModel', () => { let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; @@ -22,7 +22,7 @@ suite('Workbench editor model', () => { accessor = instantiationService.createInstance(TestServiceAccessor); }); - test('TextDiffEditorModel', async () => { + test('basics', async () => { const dispose = accessor.textModelResolverService.registerTextModelContentProvider('test', { provideTextContent: async function (resource: URI): Promise { if (resource.scheme === 'test') { @@ -36,8 +36,8 @@ suite('Workbench editor model', () => { } }); - let input = instantiationService.createInstance(ResourceEditorInput, URI.from({ scheme: 'test', authority: null!, path: 'thePath' }), 'name', 'description', undefined); - let otherInput = instantiationService.createInstance(ResourceEditorInput, URI.from({ scheme: 'test', authority: null!, path: 'thePath' }), 'name2', 'description', undefined); + let input = instantiationService.createInstance(TextResourceEditorInput, URI.from({ scheme: 'test', authority: null!, path: 'thePath' }), 'name', 'description', undefined, undefined); + let otherInput = instantiationService.createInstance(TextResourceEditorInput, URI.from({ scheme: 'test', authority: null!, path: 'thePath' }), 'name2', 'description', undefined, undefined); let diffInput = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined); let model = await diffInput.resolve() as TextDiffEditorModel; diff --git a/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts index a7f21bc5ab..f87b0cd5c0 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { EditorGroupModel, ISerializedEditorGroupModel, EditorCloseEvent } from 'vs/workbench/common/editor/editorGroupModel'; -import { EditorExtensions, IEditorInputFactoryRegistry, EditorInput, IFileEditorInput, IEditorInputSerializer, CloseDirection, EditorsOrder } from 'vs/workbench/common/editor'; +import { EditorExtensions, IEditorInputFactoryRegistry, IFileEditorInput, IEditorInputSerializer, CloseDirection, EditorsOrder } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; import { TestLifecycleService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; @@ -22,8 +22,9 @@ import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { TestContextService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -suite('Workbench editor group model', () => { +suite('EditorGroupModel', () => { function inst(): IInstantiationService { let inst = new TestInstantiationService(); @@ -166,6 +167,7 @@ suite('Workbench editor group model', () => { getEncoding() { return undefined; } setPreferredEncoding(encoding: string) { } setForceOpenAsBinary(): void { } + setPreferredContents(contents: string): void { } setMode(mode: string) { } setPreferredMode(mode: string) { } isResolved(): boolean { return false; } @@ -256,7 +258,7 @@ suite('Workbench editor group model', () => { assert.strictEqual(clone.count, 3); let didEditorLabelChange = false; - const toDispose = clone.onDidEditorLabelChange(() => didEditorLabelChange = true); + const toDispose = clone.onDidChangeEditorLabel(() => didEditorLabelChange = true); input1.setLabel(); assert.ok(didEditorLabelChange); @@ -1558,12 +1560,12 @@ suite('Workbench editor group model', () => { }); let label1ChangeCounter = 0; - group1.onDidEditorLabelChange(() => { + group1.onDidChangeEditorLabel(() => { label1ChangeCounter++; }); let label2ChangeCounter = 0; - group2.onDidEditorLabelChange(() => { + group2.onDidChangeEditorLabel(() => { label2ChangeCounter++; }); diff --git a/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts index 13874ea879..f43e5b8071 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts @@ -4,11 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { EditorInput } from 'vs/workbench/common/editor'; -import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; -import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -suite('Workbench editor input', () => { +suite('EditorInput', () => { class MyEditorInput extends EditorInput { readonly resource = undefined; @@ -17,7 +15,7 @@ suite('Workbench editor input', () => { override resolve(): any { return null; } } - test('EditorInput', () => { + test('basics', () => { let counter = 0; let input = new MyEditorInput(); let otherInput = new MyEditorInput(); @@ -35,60 +33,4 @@ suite('Workbench editor input', () => { input.dispose(); assert.strictEqual(counter, 1); }); - - test('DiffEditorInput', () => { - const instantiationService = workbenchInstantiationService(); - - let counter = 0; - let input = new MyEditorInput(); - input.onWillDispose(() => { - assert(true); - counter++; - }); - - let otherInput = new MyEditorInput(); - otherInput.onWillDispose(() => { - assert(true); - counter++; - }); - - let diffInput = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined); - - assert.strictEqual(diffInput.originalInput, input); - assert.strictEqual(diffInput.modifiedInput, otherInput); - assert(diffInput.matches(diffInput)); - assert(!diffInput.matches(otherInput)); - assert(!diffInput.matches(null)); - - diffInput.dispose(); - assert.strictEqual(counter, 0); - }); - - test('DiffEditorInput disposes when input inside disposes', function () { - const instantiationService = workbenchInstantiationService(); - - let counter = 0; - let input = new MyEditorInput(); - let otherInput = new MyEditorInput(); - - let diffInput = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined); - diffInput.onWillDispose(() => { - counter++; - assert(true); - }); - - input.dispose(); - - input = new MyEditorInput(); - otherInput = new MyEditorInput(); - - let diffInput2 = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined); - diffInput2.onWillDispose(() => { - counter++; - assert(true); - }); - - otherInput.dispose(); - assert.strictEqual(counter, 2); - }); }); diff --git a/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts index ba447b386c..75778cc06b 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts @@ -5,7 +5,6 @@ import * as assert from 'assert'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { EditorModel } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; @@ -26,8 +25,9 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { TestTextResourcePropertiesService } from 'vs/workbench/test/common/workbenchTestServices'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; -suite('Workbench editor model', () => { +suite('EditorModel', () => { class MyEditorModel extends EditorModel { } class MyTextEditorModel extends BaseTextEditorModel { @@ -62,7 +62,7 @@ suite('Workbench editor model', () => { modeService = instantiationService.stub(IModeService, ModeServiceImpl); }); - test('EditorModel', async () => { + test('basics', async () => { let counter = 0; const model = new MyEditorModel(); diff --git a/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts index b51d195f3d..0deb1b96d7 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts @@ -5,27 +5,34 @@ import * as assert from 'assert'; import { EditorPane, EditorMemento } from 'vs/workbench/browser/parts/editor/editorPane'; -import { EditorInput, EditorOptions, IEditorInputSerializer, IEditorInputFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor'; +import { WorkspaceTrustRequiredEditor } from 'vs/workbench/browser/parts/editor/workspaceTrustRequiredEditor'; +import { IEditorInputSerializer, IEditorInputFactoryRegistry, EditorExtensions, EditorInputCapabilities, IEditorDescriptor, IEditorPane } from 'vs/workbench/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { workbenchInstantiationService, TestEditorGroupView, TestEditorGroupsService, registerTestResourceEditor, TestEditorInput } from 'vs/workbench/test/browser/workbenchTestServices'; -import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; +import { workbenchInstantiationService, TestEditorGroupView, TestEditorGroupsService, registerTestResourceEditor, TestEditorInput, createEditorPart } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { URI } from 'vs/base/common/uri'; -import { IEditorRegistry, EditorDescriptor } from 'vs/workbench/browser/editor'; +import { EditorDescriptor, EditorRegistry } from 'vs/workbench/browser/editor'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorModel } from 'vs/platform/editor/common/editor'; import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { extUri } from 'vs/base/common/resources'; +import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { TestWorkspaceTrustManagementService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; const NullThemeService = new TestThemeService(); -let EditorRegistry: IEditorRegistry = Registry.as(EditorExtensions.Editors); -let EditorInputRegistry: IEditorInputFactoryRegistry = Registry.as(EditorExtensions.EditorInputFactories); +const editorRegistry: EditorRegistry = Registry.as(EditorExtensions.Editors); +const editorInputRegistry: IEditorInputFactoryRegistry = Registry.as(EditorExtensions.EditorInputFactories); class TestEditor extends EditorPane { @@ -69,8 +76,8 @@ class TestInput extends EditorInput { readonly resource = undefined; - override getPreferredEditorId(ids: string[]) { - return ids[1]; + override prefersEditor>(editors: T[]): T | undefined { + return editors[1]; } override get typeId(): string { @@ -94,90 +101,90 @@ class OtherTestInput extends EditorInput { return null; } } -class TestResourceEditorInput extends ResourceEditorInput { } +class TestResourceEditorInput extends TextResourceEditorInput { } -suite('Workbench EditorPane', () => { +suite('EditorPane', () => { test('EditorPane API', async () => { - let e = new TestEditor(NullTelemetryService); - let input = new OtherTestInput(); - let options = new EditorOptions(); + const editor = new TestEditor(NullTelemetryService); + const input = new OtherTestInput(); + const options = {}; - assert(!e.isVisible()); - assert(!e.input); + assert(!editor.isVisible()); + assert(!editor.input); - await e.setInput(input, options, Object.create(null), CancellationToken.None); - assert.strictEqual(input, e.input); + await editor.setInput(input, options, Object.create(null), CancellationToken.None); + assert.strictEqual(input, editor.input); const group = new TestEditorGroupView(1); - e.setVisible(true, group); - assert(e.isVisible()); - assert.strictEqual(e.group, group); + editor.setVisible(true, group); + assert(editor.isVisible()); + assert.strictEqual(editor.group, group); input.onWillDispose(() => { assert(false); }); - e.dispose(); - e.clearInput(); - e.setVisible(false, group); - assert(!e.isVisible()); - assert(!e.input); - assert(!e.getControl()); + editor.dispose(); + editor.clearInput(); + editor.setVisible(false, group); + assert(!editor.isVisible()); + assert(!editor.input); + assert(!editor.getControl()); }); test('EditorDescriptor', () => { - let d = EditorDescriptor.create(TestEditor, 'id', 'name'); - assert.strictEqual(d.getId(), 'id'); - assert.strictEqual(d.getName(), 'name'); + const editorDescriptor = EditorDescriptor.create(TestEditor, 'id', 'name'); + assert.strictEqual(editorDescriptor.typeId, 'id'); + assert.strictEqual(editorDescriptor.name, 'name'); }); test('Editor Registration', function () { - let d1 = EditorDescriptor.create(TestEditor, 'id1', 'name'); - let d2 = EditorDescriptor.create(OtherTestEditor, 'id2', 'name'); + const editorDescriptor1 = EditorDescriptor.create(TestEditor, 'id1', 'name'); + const editorDescriptor2 = EditorDescriptor.create(OtherTestEditor, 'id2', 'name'); - let oldEditorsCnt = EditorRegistry.getEditors().length; - let oldInputCnt = (EditorRegistry).getEditorInputs().length; + const oldEditorsCnt = editorRegistry.getEditors().length; + const oldInputCnt = editorRegistry.getEditorInputs().length; - const dispose1 = EditorRegistry.registerEditor(d1, [new SyncDescriptor(TestInput)]); - const dispose2 = EditorRegistry.registerEditor(d2, [new SyncDescriptor(TestInput), new SyncDescriptor(OtherTestInput)]); + const dispose1 = editorRegistry.registerEditor(editorDescriptor1, [new SyncDescriptor(TestInput)]); + const dispose2 = editorRegistry.registerEditor(editorDescriptor2, [new SyncDescriptor(TestInput), new SyncDescriptor(OtherTestInput)]); - assert.strictEqual(EditorRegistry.getEditors().length, oldEditorsCnt + 2); - assert.strictEqual((EditorRegistry).getEditorInputs().length, oldInputCnt + 3); + assert.strictEqual(editorRegistry.getEditors().length, oldEditorsCnt + 2); + assert.strictEqual(editorRegistry.getEditorInputs().length, oldInputCnt + 3); - assert.strictEqual(EditorRegistry.getEditor(new TestInput()), d2); - assert.strictEqual(EditorRegistry.getEditor(new OtherTestInput()), d2); + assert.strictEqual(editorRegistry.getEditor(new TestInput()), editorDescriptor2); + assert.strictEqual(editorRegistry.getEditor(new OtherTestInput()), editorDescriptor2); - assert.strictEqual(EditorRegistry.getEditorById('id1'), d1); - assert.strictEqual(EditorRegistry.getEditorById('id2'), d2); - assert(!EditorRegistry.getEditorById('id3')); + assert.strictEqual(editorRegistry.getEditorByType('id1'), editorDescriptor1); + assert.strictEqual(editorRegistry.getEditorByType('id2'), editorDescriptor2); + assert(!editorRegistry.getEditorByType('id3')); dispose([dispose1, dispose2]); }); test('Editor Lookup favors specific class over superclass (match on specific class)', function () { - let d1 = EditorDescriptor.create(TestEditor, 'id1', 'name'); + const d1 = EditorDescriptor.create(TestEditor, 'id1', 'name'); const disposables = new DisposableStore(); disposables.add(registerTestResourceEditor()); - disposables.add(EditorRegistry.registerEditor(d1, [new SyncDescriptor(TestResourceEditorInput)])); + disposables.add(editorRegistry.registerEditor(d1, [new SyncDescriptor(TestResourceEditorInput)])); - let inst = workbenchInstantiationService(); + const inst = workbenchInstantiationService(); - const editor = EditorRegistry.getEditor(inst.createInstance(TestResourceEditorInput, URI.file('/fake'), 'fake', '', undefined))!.instantiate(inst); + const editor = editorRegistry.getEditor(inst.createInstance(TestResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined))!.instantiate(inst); assert.strictEqual(editor.getId(), 'testEditor'); - const otherEditor = EditorRegistry.getEditor(inst.createInstance(ResourceEditorInput, URI.file('/fake'), 'fake', '', undefined))!.instantiate(inst); + const otherEditor = editorRegistry.getEditor(inst.createInstance(TextResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined))!.instantiate(inst); assert.strictEqual(otherEditor.getId(), 'workbench.editors.textResourceEditor'); disposables.dispose(); }); test('Editor Lookup favors specific class over superclass (match on super class)', function () { - let inst = workbenchInstantiationService(); + const inst = workbenchInstantiationService(); const disposables = new DisposableStore(); disposables.add(registerTestResourceEditor()); - const editor = EditorRegistry.getEditor(inst.createInstance(TestResourceEditorInput, URI.file('/fake'), 'fake', '', undefined))!.instantiate(inst); + const editor = editorRegistry.getEditor(inst.createInstance(TestResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined))!.instantiate(inst); assert.strictEqual('workbench.editors.textResourceEditor', editor.getId()); @@ -186,17 +193,17 @@ suite('Workbench EditorPane', () => { test('Editor Input Serializer', function () { const testInput = new TestEditorInput(URI.file('/fake'), 'testTypeId'); - workbenchInstantiationService().invokeFunction(accessor => EditorInputRegistry.start(accessor)); - const disposable = EditorInputRegistry.registerEditorInputSerializer(testInput.typeId, TestInputSerializer); + workbenchInstantiationService().invokeFunction(accessor => editorInputRegistry.start(accessor)); + const disposable = editorInputRegistry.registerEditorInputSerializer(testInput.typeId, TestInputSerializer); - let factory = EditorInputRegistry.getEditorInputSerializer('testTypeId'); + let factory = editorInputRegistry.getEditorInputSerializer('testTypeId'); assert(factory); - factory = EditorInputRegistry.getEditorInputSerializer(testInput); + factory = editorInputRegistry.getEditorInputSerializer(testInput); assert(factory); // throws when registering serializer for same type - assert.throws(() => EditorInputRegistry.registerEditorInputSerializer(testInput.typeId, TestInputSerializer)); + assert.throws(() => editorInputRegistry.registerEditorInputSerializer(testInput.typeId, TestInputSerializer)); disposable.dispose(); }); @@ -277,7 +284,7 @@ suite('Workbench EditorPane', () => { interface TestViewState { line: number; } const rawMemento = Object.create(null); - let memento = new EditorMemento('id', 'key', rawMemento, 3, editorGroupService); + const memento = new EditorMemento('id', 'key', rawMemento, 3, editorGroupService); memento.saveEditorState(testGroup0, URI.file('/some/folder/file-1.txt'), { line: 1 }); memento.saveEditorState(testGroup0, URI.file('/some/folder/file-2.txt'), { line: 2 }); @@ -320,7 +327,7 @@ suite('Workbench EditorPane', () => { } const rawMemento = Object.create(null); - let memento = new EditorMemento('id', 'key', rawMemento, 3, new TestEditorGroupsService()); + const memento = new EditorMemento('id', 'key', rawMemento, 3, new TestEditorGroupsService()); const testInputA = new TestEditorInput(URI.file('/A')); @@ -358,7 +365,7 @@ suite('Workbench EditorPane', () => { } const rawMemento = Object.create(null); - let memento = new EditorMemento('id', 'key', rawMemento, 3, new TestEditorGroupsService()); + const memento = new EditorMemento('id', 'key', rawMemento, 3, new TestEditorGroupsService()); const testInputA = new TestEditorInput(URI.file('/A')); @@ -393,4 +400,72 @@ suite('Workbench EditorPane', () => { res = memento.loadEditorState(testGroup0, testInputB); assert.ok(!res); }); + + test('WorkspaceTrustRequiredEditor', async function () { + + class TrustRequiredTestEditor extends EditorPane { + constructor(@ITelemetryService telemetryService: ITelemetryService) { + super('TestEditor', NullTelemetryService, NullThemeService, new TestStorageService()); + } + + override getId(): string { return 'trustRequiredTestEditor'; } + layout(): void { } + createEditor(): any { } + } + + class TrustRequiredTestInput extends EditorInput { + + readonly resource = undefined; + + override get typeId(): string { + return 'trustRequiredTestInput'; + } + + override get capabilities(): EditorInputCapabilities { + return EditorInputCapabilities.RequiresTrust; + } + + override resolve(): any { + return null; + } + } + + const disposables = new DisposableStore(); + + const instantiationService = workbenchInstantiationService(); + const workspaceTrustService = instantiationService.createInstance(TestWorkspaceTrustManagementService); + instantiationService.stub(IWorkspaceTrustManagementService, workspaceTrustService); + workspaceTrustService.setWorkspaceTrust(false); + + const editorPart = await createEditorPart(instantiationService, disposables); + instantiationService.stub(IEditorGroupsService, editorPart); + + const editorService = instantiationService.createInstance(EditorService); + instantiationService.stub(IEditorService, editorService); + + const group = editorPart.activeGroup; + + const editorDescriptor = EditorDescriptor.create(TrustRequiredTestEditor, 'id1', 'name'); + disposables.add(editorRegistry.registerEditor(editorDescriptor, [new SyncDescriptor(TrustRequiredTestInput)])); + + const testInput = new TrustRequiredTestInput(); + + await group.openEditor(testInput); + assert.strictEqual(group.activeEditorPane?.getId(), WorkspaceTrustRequiredEditor.ID); + + const getEditorPaneIdAsync = () => new Promise(resolve => { + disposables.add(editorService.onDidActiveEditorChange(event => { + resolve(group.activeEditorPane?.getId()); + })); + }); + + workspaceTrustService.setWorkspaceTrust(true); + + assert.strictEqual(await getEditorPaneIdAsync(), 'trustRequiredTestEditor'); + + workspaceTrustService.setWorkspaceTrust(false); + assert.strictEqual(await getEditorPaneIdAsync(), WorkspaceTrustRequiredEditor.ID); + + dispose(disposables); + }); }); diff --git a/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts index ff9fe1b002..f124bc7b35 100644 --- a/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts @@ -5,50 +5,51 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; -import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; -import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; -import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; -import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; +import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { AbstractResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IFileService } from 'vs/platform/files/common/files'; +import { EditorInputCapabilities, Verbosity } from 'vs/workbench/common/editor'; -suite('Resource text editors', () => { +suite('ResourceEditorInput', () => { let instantiationService: IInstantiationService; - let accessor: TestServiceAccessor; + + class TestResourceEditorInput extends AbstractResourceEditorInput { + + readonly typeId = 'test.typeId'; + + constructor( + resource: URI, + @ILabelService labelService: ILabelService, + @IFileService fileService: IFileService + ) { + super(resource, resource, labelService, fileService); + } + } setup(() => { instantiationService = workbenchInstantiationService(); - accessor = instantiationService.createInstance(TestServiceAccessor); }); test('basics', async () => { - const resource = URI.from({ scheme: 'inmemory', authority: null!, path: 'thePath' }); - accessor.modelService.createModel('function test() {}', accessor.modeService.create('text'), resource); + const resource = URI.from({ scheme: 'testResource', path: 'thePath/of/the/resource.txt' }); - const input: ResourceEditorInput = instantiationService.createInstance(ResourceEditorInput, resource, 'The Name', 'The Description', undefined); + const input = instantiationService.createInstance(TestResourceEditorInput, resource); - const model = await input.resolve(); + assert.ok(input.getName().length > 0); - assert.ok(model); - assert.strictEqual(snapshotToString(((model as ResourceEditorModel).createSnapshot()!)), 'function test() {}'); - }); + assert.ok(input.getDescription(Verbosity.SHORT)!.length > 0); + assert.ok(input.getDescription(Verbosity.MEDIUM)!.length > 0); + assert.ok(input.getDescription(Verbosity.LONG)!.length > 0); - test('custom mode', async () => { - ModesRegistry.registerLanguage({ - id: 'resource-input-test', - }); + assert.ok(input.getTitle(Verbosity.SHORT).length > 0); + assert.ok(input.getTitle(Verbosity.MEDIUM).length > 0); + assert.ok(input.getTitle(Verbosity.LONG).length > 0); - const resource = URI.from({ scheme: 'inmemory', authority: null!, path: 'thePath' }); - accessor.modelService.createModel('function test() {}', accessor.modeService.create('text'), resource); - - const input: ResourceEditorInput = instantiationService.createInstance(ResourceEditorInput, resource, 'The Name', 'The Description', 'resource-input-test'); - - const model = await input.resolve(); - assert.ok(model); - assert.strictEqual(model.textEditorModel?.getModeId(), 'resource-input-test'); - - input.setMode('text'); - assert.strictEqual(model.textEditorModel?.getModeId(), PLAINTEXT_MODE_ID); + assert.strictEqual(input.hasCapability(EditorInputCapabilities.Readonly), false); + assert.strictEqual(input.hasCapability(EditorInputCapabilities.Untitled), true); + assert.strictEqual(input.isOrphaned(), false); }); }); diff --git a/src/vs/workbench/test/browser/parts/editor/sideBySideEditorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/sideBySideEditorInput.test.ts new file mode 100644 index 0000000000..5c55ca69b5 --- /dev/null +++ b/src/vs/workbench/test/browser/parts/editor/sideBySideEditorInput.test.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 * as assert from 'assert'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; + +suite('SideBySideEditorInput', () => { + + class MyEditorInput extends EditorInput { + readonly resource = undefined; + + override get typeId(): string { return 'myEditorInput'; } + override resolve(): any { return null; } + + fireCapabilitiesChangeEvent(): void { + this._onDidChangeCapabilities.fire(); + } + + fireDirtyChangeEvent(): void { + this._onDidChangeDirty.fire(); + } + + fireLabelChangeEvent(): void { + this._onDidChangeLabel.fire(); + } + } + + test('events dispatching', () => { + let input = new MyEditorInput(); + let otherInput = new MyEditorInput(); + + const sideBySideInut = new SideBySideEditorInput('name', 'description', otherInput, input); + + let capabilitiesChangeCounter = 0; + sideBySideInut.onDidChangeCapabilities(() => capabilitiesChangeCounter++); + + let dirtyChangeCounter = 0; + sideBySideInut.onDidChangeDirty(() => dirtyChangeCounter++); + + let labelChangeCounter = 0; + sideBySideInut.onDidChangeLabel(() => labelChangeCounter++); + + input.fireCapabilitiesChangeEvent(); + assert.strictEqual(capabilitiesChangeCounter, 1); + + otherInput.fireCapabilitiesChangeEvent(); + assert.strictEqual(capabilitiesChangeCounter, 2); + + input.fireDirtyChangeEvent(); + otherInput.fireDirtyChangeEvent(); + assert.strictEqual(dirtyChangeCounter, 1); + + input.fireLabelChangeEvent(); + otherInput.fireLabelChangeEvent(); + assert.strictEqual(labelChangeCounter, 1); + }); + +}); diff --git a/src/vs/workbench/test/browser/parts/editor/textResourceEditorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/textResourceEditorInput.test.ts new file mode 100644 index 0000000000..a3484b326c --- /dev/null +++ b/src/vs/workbench/test/browser/parts/editor/textResourceEditorInput.test.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 assert from 'assert'; +import { URI } from 'vs/base/common/uri'; +import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; +import { TextResourceEditorModel } from 'vs/workbench/common/editor/textResourceEditorModel'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; +import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; +import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; + +suite('TextResourceEditorInput', () => { + + let instantiationService: IInstantiationService; + let accessor: TestServiceAccessor; + + setup(() => { + instantiationService = workbenchInstantiationService(); + accessor = instantiationService.createInstance(TestServiceAccessor); + }); + + test('basics', async () => { + const resource = URI.from({ scheme: 'inmemory', authority: null!, path: 'thePath' }); + accessor.modelService.createModel('function test() {}', accessor.modeService.create('text'), resource); + + const input = instantiationService.createInstance(TextResourceEditorInput, resource, 'The Name', 'The Description', undefined, undefined); + + const model = await input.resolve(); + + assert.ok(model); + assert.strictEqual(snapshotToString(((model as TextResourceEditorModel).createSnapshot()!)), 'function test() {}'); + }); + + test('preferred mode (via ctor)', async () => { + ModesRegistry.registerLanguage({ + id: 'resource-input-test', + }); + + const resource = URI.from({ scheme: 'inmemory', authority: null!, path: 'thePath' }); + accessor.modelService.createModel('function test() {}', accessor.modeService.create('text'), resource); + + const input = instantiationService.createInstance(TextResourceEditorInput, resource, 'The Name', 'The Description', 'resource-input-test', undefined); + + const model = await input.resolve(); + assert.ok(model); + assert.strictEqual(model.textEditorModel?.getModeId(), 'resource-input-test'); + + input.setMode('text'); + assert.strictEqual(model.textEditorModel?.getModeId(), PLAINTEXT_MODE_ID); + + await input.resolve(); + assert.strictEqual(model.textEditorModel?.getModeId(), PLAINTEXT_MODE_ID); + }); + + test('preferred mode (via setPreferredMode)', async () => { + ModesRegistry.registerLanguage({ + id: 'resource-input-test', + }); + + const resource = URI.from({ scheme: 'inmemory', authority: null!, path: 'thePath' }); + accessor.modelService.createModel('function test() {}', accessor.modeService.create('text'), resource); + + const input = instantiationService.createInstance(TextResourceEditorInput, resource, 'The Name', 'The Description', undefined, undefined); + input.setPreferredMode('resource-input-test'); + + const model = await input.resolve(); + assert.ok(model); + assert.strictEqual(model.textEditorModel?.getModeId(), 'resource-input-test'); + }); + + test('preferred contents (via ctor)', async () => { + const resource = URI.from({ scheme: 'inmemory', authority: null!, path: 'thePath' }); + accessor.modelService.createModel('function test() {}', accessor.modeService.create('text'), resource); + + const input = instantiationService.createInstance(TextResourceEditorInput, resource, 'The Name', 'The Description', undefined, 'My Resource Input Contents'); + + const model = await input.resolve(); + assert.ok(model); + assert.strictEqual(model.textEditorModel?.getValue(), 'My Resource Input Contents'); + + model.textEditorModel.setValue('Some other contents'); + assert.strictEqual(model.textEditorModel?.getValue(), 'Some other contents'); + + await input.resolve(); + assert.strictEqual(model.textEditorModel?.getValue(), 'Some other contents'); // preferred contents only used once + }); + + test('preferred contents (via setPreferredContents)', async () => { + const resource = URI.from({ scheme: 'inmemory', authority: null!, path: 'thePath' }); + accessor.modelService.createModel('function test() {}', accessor.modeService.create('text'), resource); + + const input = instantiationService.createInstance(TextResourceEditorInput, resource, 'The Name', 'The Description', undefined, undefined); + input.setPreferredContents('My Resource Input Contents'); + + const model = await input.resolve(); + assert.ok(model); + assert.strictEqual(model.textEditorModel?.getValue(), 'My Resource Input Contents'); + + model.textEditorModel.setValue('Some other contents'); + assert.strictEqual(model.textEditorModel?.getValue(), 'Some other contents'); + + await input.resolve(); + assert.strictEqual(model.textEditorModel?.getValue(), 'Some other contents'); // preferred contents only used once + }); +}); diff --git a/src/vs/workbench/test/browser/quickAccess.test.ts b/src/vs/workbench/test/browser/quickAccess.test.ts new file mode 100644 index 0000000000..ef70d23df3 --- /dev/null +++ b/src/vs/workbench/test/browser/quickAccess.test.ts @@ -0,0 +1,331 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Registry } from 'vs/platform/registry/common/platform'; +import { IQuickAccessRegistry, Extensions, IQuickAccessProvider, QuickAccessRegistry } from 'vs/platform/quickinput/common/quickAccess'; +import { IQuickPick, IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { DisposableStore, toDisposable, IDisposable } from 'vs/base/common/lifecycle'; +import { timeout } from 'vs/base/common/async'; +import { PickerQuickAccessProvider, FastAndSlowPicks } from 'vs/platform/quickinput/browser/pickerQuickAccess'; + +suite('QuickAccess', () => { + + let instantiationService: IInstantiationService; + let accessor: TestServiceAccessor; + + let providerDefaultCalled = false; + let providerDefaultCanceled = false; + let providerDefaultDisposed = false; + + let provider1Called = false; + let provider1Canceled = false; + let provider1Disposed = false; + + let provider2Called = false; + let provider2Canceled = false; + let provider2Disposed = false; + + let provider3Called = false; + let provider3Canceled = false; + let provider3Disposed = false; + + class TestProviderDefault implements IQuickAccessProvider { + + constructor(@IQuickInputService private readonly quickInputService: IQuickInputService, disposables: DisposableStore) { } + + provide(picker: IQuickPick, token: CancellationToken): IDisposable { + assert.ok(picker); + providerDefaultCalled = true; + token.onCancellationRequested(() => providerDefaultCanceled = true); + + // bring up provider #3 + setTimeout(() => this.quickInputService.quickAccess.show(providerDescriptor3.prefix)); + + return toDisposable(() => providerDefaultDisposed = true); + } + } + + class TestProvider1 implements IQuickAccessProvider { + provide(picker: IQuickPick, token: CancellationToken): IDisposable { + assert.ok(picker); + provider1Called = true; + token.onCancellationRequested(() => provider1Canceled = true); + + return toDisposable(() => provider1Disposed = true); + } + } + + class TestProvider2 implements IQuickAccessProvider { + provide(picker: IQuickPick, token: CancellationToken): IDisposable { + assert.ok(picker); + provider2Called = true; + token.onCancellationRequested(() => provider2Canceled = true); + + return toDisposable(() => provider2Disposed = true); + } + } + + class TestProvider3 implements IQuickAccessProvider { + provide(picker: IQuickPick, token: CancellationToken): IDisposable { + assert.ok(picker); + provider3Called = true; + token.onCancellationRequested(() => provider3Canceled = true); + + // hide without picking + setTimeout(() => picker.hide()); + + return toDisposable(() => provider3Disposed = true); + } + } + + const providerDescriptorDefault = { ctor: TestProviderDefault, prefix: '', helpEntries: [] }; + const providerDescriptor1 = { ctor: TestProvider1, prefix: 'test', helpEntries: [] }; + const providerDescriptor2 = { ctor: TestProvider2, prefix: 'test something', helpEntries: [] }; + const providerDescriptor3 = { ctor: TestProvider3, prefix: 'changed', helpEntries: [] }; + + setup(() => { + instantiationService = workbenchInstantiationService(); + accessor = instantiationService.createInstance(TestServiceAccessor); + }); + + test('registry', () => { + const registry = (Registry.as(Extensions.Quickaccess)); + const restore = (registry as QuickAccessRegistry).clear(); + + assert.ok(!registry.getQuickAccessProvider('test')); + + const disposables = new DisposableStore(); + + disposables.add(registry.registerQuickAccessProvider(providerDescriptorDefault)); + assert(registry.getQuickAccessProvider('') === providerDescriptorDefault); + assert(registry.getQuickAccessProvider('test') === providerDescriptorDefault); + + const disposable = disposables.add(registry.registerQuickAccessProvider(providerDescriptor1)); + assert(registry.getQuickAccessProvider('test') === providerDescriptor1); + + const providers = registry.getQuickAccessProviders(); + assert(providers.some(provider => provider.prefix === 'test')); + + disposable.dispose(); + assert(registry.getQuickAccessProvider('test') === providerDescriptorDefault); + + disposables.dispose(); + assert.ok(!registry.getQuickAccessProvider('test')); + + restore(); + }); + + test('provider', async () => { + const registry = (Registry.as(Extensions.Quickaccess)); + const restore = (registry as QuickAccessRegistry).clear(); + + const disposables = new DisposableStore(); + + disposables.add(registry.registerQuickAccessProvider(providerDescriptorDefault)); + disposables.add(registry.registerQuickAccessProvider(providerDescriptor1)); + disposables.add(registry.registerQuickAccessProvider(providerDescriptor2)); + disposables.add(registry.registerQuickAccessProvider(providerDescriptor3)); + + accessor.quickInputService.quickAccess.show('test'); + assert.strictEqual(providerDefaultCalled, false); + assert.strictEqual(provider1Called, true); + assert.strictEqual(provider2Called, false); + assert.strictEqual(provider3Called, false); + assert.strictEqual(providerDefaultCanceled, false); + assert.strictEqual(provider1Canceled, false); + assert.strictEqual(provider2Canceled, false); + assert.strictEqual(provider3Canceled, false); + assert.strictEqual(providerDefaultDisposed, false); + assert.strictEqual(provider1Disposed, false); + assert.strictEqual(provider2Disposed, false); + assert.strictEqual(provider3Disposed, false); + provider1Called = false; + + accessor.quickInputService.quickAccess.show('test something'); + assert.strictEqual(providerDefaultCalled, false); + assert.strictEqual(provider1Called, false); + assert.strictEqual(provider2Called, true); + assert.strictEqual(provider3Called, false); + assert.strictEqual(providerDefaultCanceled, false); + assert.strictEqual(provider1Canceled, true); + assert.strictEqual(provider2Canceled, false); + assert.strictEqual(provider3Canceled, false); + assert.strictEqual(providerDefaultDisposed, false); + assert.strictEqual(provider1Disposed, true); + assert.strictEqual(provider2Disposed, false); + assert.strictEqual(provider3Disposed, false); + provider2Called = false; + provider1Canceled = false; + provider1Disposed = false; + + accessor.quickInputService.quickAccess.show('usedefault'); + assert.strictEqual(providerDefaultCalled, true); + assert.strictEqual(provider1Called, false); + assert.strictEqual(provider2Called, false); + assert.strictEqual(provider3Called, false); + assert.strictEqual(providerDefaultCanceled, false); + assert.strictEqual(provider1Canceled, false); + assert.strictEqual(provider2Canceled, true); + assert.strictEqual(provider3Canceled, false); + assert.strictEqual(providerDefaultDisposed, false); + assert.strictEqual(provider1Disposed, false); + assert.strictEqual(provider2Disposed, true); + assert.strictEqual(provider3Disposed, false); + + await timeout(1); + + assert.strictEqual(providerDefaultCanceled, true); + assert.strictEqual(providerDefaultDisposed, true); + assert.strictEqual(provider3Called, true); + + await timeout(1); + + assert.strictEqual(provider3Canceled, true); + assert.strictEqual(provider3Disposed, true); + + disposables.dispose(); + + restore(); + }); + + let fastProviderCalled = false; + let slowProviderCalled = false; + let fastAndSlowProviderCalled = false; + + let slowProviderCanceled = false; + let fastAndSlowProviderCanceled = false; + + class FastTestQuickPickProvider extends PickerQuickAccessProvider { + + constructor() { + super('fast'); + } + + protected _getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Array { + fastProviderCalled = true; + + return [{ label: 'Fast Pick' }]; + } + } + + class SlowTestQuickPickProvider extends PickerQuickAccessProvider { + + constructor() { + super('slow'); + } + + protected async _getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise> { + slowProviderCalled = true; + + await timeout(1); + + if (token.isCancellationRequested) { + slowProviderCanceled = true; + } + + return [{ label: 'Slow Pick' }]; + } + } + + class FastAndSlowTestQuickPickProvider extends PickerQuickAccessProvider { + + constructor() { + super('bothFastAndSlow'); + } + + protected _getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): FastAndSlowPicks { + fastAndSlowProviderCalled = true; + + return { + picks: [{ label: 'Fast Pick' }], + additionalPicks: (async () => { + await timeout(1); + + if (token.isCancellationRequested) { + fastAndSlowProviderCanceled = true; + } + + return [{ label: 'Slow Pick' }]; + })() + }; + } + } + + const fastProviderDescriptor = { ctor: FastTestQuickPickProvider, prefix: 'fast', helpEntries: [] }; + const slowProviderDescriptor = { ctor: SlowTestQuickPickProvider, prefix: 'slow', helpEntries: [] }; + const fastAndSlowProviderDescriptor = { ctor: FastAndSlowTestQuickPickProvider, prefix: 'bothFastAndSlow', helpEntries: [] }; + + test('quick pick access - show()', async () => { + const registry = (Registry.as(Extensions.Quickaccess)); + const restore = (registry as QuickAccessRegistry).clear(); + + const disposables = new DisposableStore(); + + disposables.add(registry.registerQuickAccessProvider(fastProviderDescriptor)); + disposables.add(registry.registerQuickAccessProvider(slowProviderDescriptor)); + disposables.add(registry.registerQuickAccessProvider(fastAndSlowProviderDescriptor)); + + accessor.quickInputService.quickAccess.show('fast'); + assert.strictEqual(fastProviderCalled, true); + assert.strictEqual(slowProviderCalled, false); + assert.strictEqual(fastAndSlowProviderCalled, false); + fastProviderCalled = false; + + accessor.quickInputService.quickAccess.show('slow'); + await timeout(2); + + assert.strictEqual(fastProviderCalled, false); + assert.strictEqual(slowProviderCalled, true); + assert.strictEqual(slowProviderCanceled, false); + assert.strictEqual(fastAndSlowProviderCalled, false); + slowProviderCalled = false; + + accessor.quickInputService.quickAccess.show('bothFastAndSlow'); + await timeout(2); + + assert.strictEqual(fastProviderCalled, false); + assert.strictEqual(slowProviderCalled, false); + assert.strictEqual(fastAndSlowProviderCalled, true); + assert.strictEqual(fastAndSlowProviderCanceled, false); + fastAndSlowProviderCalled = false; + + accessor.quickInputService.quickAccess.show('slow'); + accessor.quickInputService.quickAccess.show('bothFastAndSlow'); + accessor.quickInputService.quickAccess.show('fast'); + + assert.strictEqual(fastProviderCalled, true); + assert.strictEqual(slowProviderCalled, true); + assert.strictEqual(fastAndSlowProviderCalled, true); + + await timeout(2); + assert.strictEqual(slowProviderCanceled, true); + assert.strictEqual(fastAndSlowProviderCanceled, true); + + disposables.dispose(); + + restore(); + }); + + test('quick pick access - pick()', async () => { + const registry = (Registry.as(Extensions.Quickaccess)); + const restore = (registry as QuickAccessRegistry).clear(); + + const disposables = new DisposableStore(); + + disposables.add(registry.registerQuickAccessProvider(fastProviderDescriptor)); + + const result = accessor.quickInputService.quickAccess.pick('fast'); + assert.strictEqual(fastProviderCalled, true); + assert.ok(result instanceof Promise); + + disposables.dispose(); + + restore(); + }); +}); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index ad686dfe40..1d4cbe7358 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -3,26 +3,27 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { basename } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { IEditorInputWithOptions, IEditorIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorInput, IEditorPane, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorInput, EditorOptions, EditorsOrder, IFileEditorInput, IEditorInputFactoryRegistry, IEditorInputSerializer, EditorExtensions, ISaveOptions, IMoveResult, ITextEditorPane, ITextDiffEditorPane, IVisibleEditorPane, IEditorOpenContext, SideBySideEditorInput, IEditorMoveEvent, EditorExtensions as Extensions } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { IEditorInputWithOptions, IEditorIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorInput, IEditorPane, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorsOrder, IFileEditorInput, IEditorInputFactoryRegistry, IEditorInputSerializer, EditorExtensions, ISaveOptions, IMoveResult, ITextEditorPane, ITextDiffEditorPane, IVisibleEditorPane, IEditorOpenContext, IEditorMoveEvent, EditorExtensions as Extensions, EditorInputCapabilities, IEditorOpenEvent } from 'vs/workbench/common/editor'; import { EditorServiceImpl, IEditorGroupView, IEditorGroupsAccessor, IEditorGroupTitleHeight } from 'vs/workbench/browser/parts/editor/editor'; import { Event, Emitter } from 'vs/base/common/event'; -import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { IResolvedWorkingCopyBackup, IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService, Parts, Position as PartPosition } from 'vs/workbench/services/layout/browser/layoutService'; import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { IEditorOptions, IResourceEditorInput, IEditorModel, ITextEditorOptions, IResourceEditorInputIdentifier } from 'vs/platform/editor/common/editor'; +import { IEditorOptions, IResourceEditorInput, IEditorModel, IResourceEditorInputIdentifier } from 'vs/platform/editor/common/editor'; import { IUntitledTextEditorService, UntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ILifecycleService, BeforeShutdownEvent, ShutdownReason, StartupKind, LifecyclePhase, WillShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { FileOperationEvent, IFileService, IFileStat, IResolveFileResult, FileChangesEvent, IResolveFileOptions, ICreateFileOptions, IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, IFileStatWithMetadata, IResolveMetadataFileOptions, IWriteFileOptions, IReadFileOptions, IFileContent, IFileStreamContent, FileOperationError, IFileSystemProviderWithFileReadStreamCapability, FileReadStreamOptions, IReadFileStreamOptions } from 'vs/platform/files/common/files'; +import { FileOperationEvent, IFileService, IFileStat, IResolveFileResult, FileChangesEvent, IResolveFileOptions, ICreateFileOptions, IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, IFileStatWithMetadata, IResolveMetadataFileOptions, IWriteFileOptions, IReadFileOptions, IFileContent, IFileStreamContent, FileOperationError, IFileSystemProviderWithFileReadStreamCapability, FileReadStreamOptions, IReadFileStreamOptions, IFileSystemProviderCapabilitiesChangeEvent } from 'vs/platform/files/common/files'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; @@ -42,7 +43,7 @@ import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions' import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { MockContextKeyService, MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { ITextBufferFactory, DefaultEndOfLine, EndOfLinePreference, ITextSnapshot } from 'vs/editor/common/model'; -import { Range } from 'vs/editor/common/core/range'; +import { IRange, Range } from 'vs/editor/common/core/range'; import { IDialogService, IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; @@ -51,7 +52,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IDecorationsService, IResourceDecorationChangeEvent, IDecoration, IDecorationData, IDecorationsProvider } from 'vs/workbench/services/decorations/browser/decorations'; import { IDisposable, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IAddGroupOptions, IMergeGroupOptions, IEditorReplacement, IGroupChangeEvent, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions, GroupOrientation, ICloseAllEditorsOptions, ICloseEditorsFilter } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IEditorService, IOpenEditorOverrideHandler, ISaveEditorsOptions, IRevertAllEditorsOptions, IResourceEditorInputType, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE, IOpenEditorOverrideEntry } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorService, ISaveEditorsOptions, IRevertAllEditorsOptions, IResourceEditorInputType, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorRegistry, EditorDescriptor } from 'vs/workbench/browser/editor'; import { Dimension, IDimension } from 'vs/base/browser/dom'; @@ -74,7 +75,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import product from 'vs/platform/product/common/product'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IWorkingCopyService, WorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopyBackupMeta, IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { IFilesConfigurationService, FilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; import { BrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; @@ -83,7 +84,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { Direction } from 'vs/base/browser/ui/grid/grid'; -import { IProgressService, IProgressOptions, IProgressWindowOptions, IProgressNotificationOptions, IProgressCompositeOptions, IProgress, IProgressStep, Progress } from 'vs/platform/progress/common/progress'; +import { IProgressService, IProgressOptions, IProgressWindowOptions, IProgressNotificationOptions, IProgressCompositeOptions, IProgress, IProgressStep, Progress, IProgressDialogOptions } from 'vs/platform/progress/common/progress'; import { IWorkingCopyFileService, WorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; @@ -96,7 +97,7 @@ import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogSer import { CodeEditorService } from 'vs/workbench/services/editor/browser/codeEditorService'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IDiffEditor, IEditor } from 'vs/editor/common/editorCommon'; +import { IChange, IDiffEditor, IEditor } from 'vs/editor/common/editorCommon'; import { IInputBox, IInputOptions, IPickOptions, IQuickInputButton, IQuickInputService, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { QuickInputService } from 'vs/workbench/services/quickinput/browser/quickInputService'; import { IListService } from 'vs/platform/list/browser/listService'; @@ -119,34 +120,38 @@ import { FileService } from 'vs/platform/files/common/fileService'; import { TextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor'; import { TestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor'; -import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; +import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { SideBySideEditor } from 'vs/workbench/browser/parts/editor/sideBySideEditor'; import { IEnterWorkspaceResult, IRecent, IRecentlyOpened, IWorkspaceFolderCreationData, IWorkspaceIdentifier, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; -import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; -import { TestWorkspaceTrustManagementService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; -import { ILocalTerminalService, IShellLaunchConfig, ITerminalChildProcess, ITerminalsLayoutInfo, ITerminalsLayoutInfoById } from 'vs/platform/terminal/common/terminal'; +import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; +import { TestWorkspaceTrustManagementService, TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; +import { ILocalTerminalService, IShellLaunchConfig, ITerminalChildProcess, ITerminalProfile, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalShellType } from 'vs/platform/terminal/common/terminal'; import { IProcessDetails, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { isArray } from 'vs/base/common/types'; -import { IShellLaunchConfigResolveOptions, ITerminalProfile, ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IShellLaunchConfigResolveOptions, ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; import { EditorOverrideService } from 'vs/workbench/services/editor/browser/editorOverrideService'; import { FILE_EDITOR_INPUT_ID } from 'vs/workbench/contrib/files/common/files'; import { IEditorOverrideService } from 'vs/workbench/services/editor/common/editorOverrideService'; import { IWorkingCopyEditorService, WorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; import { IElevatedFileService } from 'vs/workbench/services/files/common/elevatedFileService'; import { BrowserElevatedFileService } from 'vs/workbench/services/files/browser/elevatedFileService'; +import { IDiffComputationResult, IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; +import { TextEdit, IInplaceReplaceSupportResult } from 'vs/editor/common/modes'; +import { ResourceMap } from 'vs/base/common/map'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { - return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined, undefined, undefined); + return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined, undefined, undefined, undefined); } Registry.as(EditorExtensions.EditorInputFactories).registerFileEditorInputFactory({ typeId: FILE_EDITOR_INPUT_ID, - createFileEditorInput: (resource, preferredResource, preferredName, preferredDescription, preferredEncoding, preferredMode, instantiationService): IFileEditorInput => { - return instantiationService.createInstance(FileEditorInput, resource, preferredResource, preferredName, preferredDescription, preferredEncoding, preferredMode); + createFileEditorInput: (resource, preferredResource, preferredName, preferredDescription, preferredEncoding, preferredMode, preferredContents, instantiationService): IFileEditorInput => { + return instantiationService.createInstance(FileEditorInput, resource, preferredResource, preferredName, preferredDescription, preferredEncoding, preferredMode, preferredContents); }, isFileEditorInput: (obj): obj is IFileEditorInput => { @@ -183,6 +188,7 @@ export function workbenchInstantiationService( ): ITestInstantiationService { const instantiationService = new TestInstantiationService(new ServiceCollection([ILifecycleService, new TestLifecycleService()])); + instantiationService.stub(IEditorWorkerService, new TestEditorWorkerService()); instantiationService.stub(IWorkingCopyService, disposables.add(new WorkingCopyService())); instantiationService.stub(IEnvironmentService, TestEnvironmentService); instantiationService.stub(IWorkbenchEnvironmentService, TestEnvironmentService); @@ -223,7 +229,6 @@ export function workbenchInstantiationService( instantiationService.stub(IKeybindingService, keybindingService); instantiationService.stub(IDecorationsService, new TestDecorationsService()); instantiationService.stub(IExtensionService, new TestExtensionService()); - instantiationService.stub(IWorkingCopyEditorService, disposables.add(instantiationService.createInstance(WorkingCopyEditorService))); instantiationService.stub(IWorkingCopyFileService, disposables.add(instantiationService.createInstance(WorkingCopyFileService))); instantiationService.stub(ITextFileService, overrides?.textFileService ? overrides.textFileService(instantiationService) : disposables.add(instantiationService.createInstance(TestTextFileService))); instantiationService.stub(IHostService, instantiationService.createInstance(TestHostService)); @@ -234,6 +239,7 @@ export function workbenchInstantiationService( instantiationService.stub(ILabelService, disposables.add(instantiationService.createInstance(LabelService))); const editorService = overrides?.editorService ? overrides.editorService(instantiationService) : new TestEditorService(editorGroupService); instantiationService.stub(IEditorService, editorService); + instantiationService.stub(IWorkingCopyEditorService, disposables.add(instantiationService.createInstance(WorkingCopyEditorService))); instantiationService.stub(IEditorOverrideService, disposables.add(instantiationService.createInstance(EditorOverrideService))); instantiationService.stub(ICodeEditorService, disposables.add(new CodeEditorService(editorService, themeService, configService))); instantiationService.stub(IViewletService, new TestViewletService()); @@ -258,9 +264,13 @@ export class TestServiceAccessor { @IModelService public modelService: ModelServiceImpl, @IFileService public fileService: TestFileService, @IFileDialogService public fileDialogService: TestFileDialogService, + @IDialogService public dialogService: TestDialogService, @IWorkingCopyService public workingCopyService: IWorkingCopyService, @IEditorService public editorService: TestEditorService, + @IWorkbenchEnvironmentService public environmentService: IWorkbenchEnvironmentService, + @IPathService public pathService: IPathService, @IEditorGroupsService public editorGroupService: IEditorGroupsService, + @IEditorOverrideService public editorOverrideService: IEditorOverrideService, @IModeService public modeService: IModeService, @ITextModelService public textModelResolverService: ITextModelService, @IUntitledTextEditorService public untitledTextEditorService: UntitledTextEditorService, @@ -275,7 +285,8 @@ export class TestServiceAccessor { @INotificationService public notificationService: INotificationService, @IWorkingCopyEditorService public workingCopyEditorService: IWorkingCopyEditorService, @IInstantiationService public instantiationService: IInstantiationService, - @IElevatedFileService public elevatedFileService: IElevatedFileService + @IElevatedFileService public elevatedFileService: IElevatedFileService, + @IWorkspaceTrustRequestService public workspaceTrustRequestService: TestWorkspaceTrustRequestService ) { } } @@ -321,8 +332,8 @@ export class TestTextFileService extends BrowserTextFileService { workingCopyFileService, uriIdentityService, modeService, - logService, - elevatedFileService + elevatedFileService, + logService ); } @@ -347,7 +358,8 @@ export class TestTextFileService extends BrowserTextFileService { etag: content.etag, encoding: 'utf8', value: await createTextBufferFactoryFromStream(content.value), - size: 10 + size: 10, + readonly: false }; } @@ -405,7 +417,7 @@ export class TestProgressService implements IProgressService { declare readonly _serviceBrand: undefined; withProgress( - options: IProgressOptions | IProgressWindowOptions | IProgressNotificationOptions | IProgressCompositeOptions, + options: IProgressOptions | IProgressDialogOptions | IProgressWindowOptions | IProgressNotificationOptions | IProgressCompositeOptions, task: (progress: IProgress) => Promise, onDidCancel?: ((choice?: number | undefined) => void) | undefined ): Promise { @@ -532,6 +544,7 @@ export class TestLayoutService implements IWorkbenchLayoutService { isStatusBarHidden(): boolean { return false; } isActivityBarHidden(): boolean { return false; } setActivityBarHidden(_hidden: boolean): void { } + setBannerHidden(_hidden: boolean): void { } isSideBarHidden(): boolean { return false; } async setEditorHidden(_hidden: boolean): Promise { } async setSideBarHidden(_hidden: boolean): Promise { } @@ -650,7 +663,6 @@ export class TestEditorGroupsService implements IEditorGroupsService { get activeGroup(): IEditorGroup { return this.groups[0]; } get count(): number { return this.groups.length; } - isRestored(): boolean { return true; } getGroups(_order?: GroupsOrder): readonly IEditorGroup[] { return this.groups; } getGroup(identifier: number): IEditorGroup | undefined { return this.groups.find(group => group.id === identifier); } getLabel(_identifier: number): string { return 'Group 1'; } @@ -709,6 +721,7 @@ export class TestEditorGroupView implements IEditorGroupView { onDidFocus: Event = Event.None; onDidChange: Event<{ width: number; height: number; }> = Event.None; onWillMoveEditor: Event = Event.None; + onWillOpenEditor: Event = Event.None; getEditors(_order?: EditorsOrder): readonly IEditorInput[] { return []; } findEditors(_resource: URI): readonly IEditorInput[] { return []; } @@ -720,8 +733,8 @@ export class TestEditorGroupView implements IEditorGroupView { isSticky(_editor: IEditorInput): boolean { return false; } isActive(_editor: IEditorInput): boolean { return false; } contains(candidate: IEditorInput): boolean { return false; } - moveEditor(_editor: IEditorInput, _target: IEditorGroup, _options?: IEditorOptions | ITextEditorOptions): void { } - copyEditor(_editor: IEditorInput, _target: IEditorGroup, _options?: IEditorOptions | ITextEditorOptions): void { } + moveEditor(_editor: IEditorInput, _target: IEditorGroup, _options?: IEditorOptions): void { } + copyEditor(_editor: IEditorInput, _target: IEditorGroup, _options?: IEditorOptions): void { } async closeEditor(_editor?: IEditorInput, options?: ICloseEditorOptions): Promise { } async closeEditors(_editors: IEditorInput[] | ICloseEditorsFilter, options?: ICloseEditorOptions): Promise { } async closeAllEditors(options?: ICloseAllEditorsOptions): Promise { } @@ -792,15 +805,13 @@ export class TestEditorService implements EditorServiceImpl { constructor(private editorGroupService?: IEditorGroupsService) { } getEditors() { return []; } findEditors() { return [] as any; } - getEditorOverrides(resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined): [IOpenEditorOverrideHandler, IOpenEditorOverrideEntry][] { return []; } - overrideOpenEditor(_handler: IOpenEditorOverrideHandler): IDisposable { return toDisposable(() => undefined); } - openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; + openEditor(editor: IEditorInput, options?: IEditorOptions, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; openEditor(editor: IResourceEditorInput | IUntitledTextResourceEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; openEditor(editor: IResourceDiffEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; - async openEditor(editor: IEditorInput | IResourceEditorInputType, optionsOrGroup?: IEditorOptions | ITextEditorOptions | IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise { + async openEditor(editor: IEditorInput | IResourceEditorInputType, optionsOrGroup?: IEditorOptions | IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise { throw new Error('not implemented'); } - doResolveEditorOpenRequest(editor: IEditorInput | IResourceEditorInputType): [IEditorGroup, EditorInput, EditorOptions | undefined] | undefined { + doResolveEditorOpenRequest(editor: IEditorInput | IResourceEditorInputType): [IEditorGroup, EditorInput, IEditorOptions | undefined] | undefined { if (!this.editorGroupService) { return undefined; } @@ -822,24 +833,31 @@ export class TestFileService implements IFileService { declare readonly _serviceBrand: undefined; private readonly _onDidFilesChange = new Emitter(); + get onDidFilesChange(): Event { return this._onDidFilesChange.event; } + fireFileChanges(event: FileChangesEvent): void { this._onDidFilesChange.fire(event); } + private readonly _onDidRunOperation = new Emitter(); + get onDidRunOperation(): Event { return this._onDidRunOperation.event; } + fireAfterOperation(event: FileOperationEvent): void { this._onDidRunOperation.fire(event); } + + private readonly _onDidChangeFileSystemProviderCapabilities = new Emitter(); + get onDidChangeFileSystemProviderCapabilities(): Event { return this._onDidChangeFileSystemProviderCapabilities.event; } + fireFileSystemProviderCapabilitiesChangeEvent(event: IFileSystemProviderCapabilitiesChangeEvent): void { this._onDidChangeFileSystemProviderCapabilities.fire(event); } readonly onWillActivateFileSystemProvider = Event.None; - readonly onDidChangeFileSystemProviderCapabilities = Event.None; readonly onError: Event = Event.None; private content = 'Hello Html'; private lastReadFileUri!: URI; + readonly = false; + setContent(content: string): void { this.content = content; } getContent(): string { return this.content; } getLastReadFileUri(): URI { return this.lastReadFileUri; } - get onDidFilesChange(): Event { return this._onDidFilesChange.event; } - fireFileChanges(event: FileChangesEvent): void { this._onDidFilesChange.fire(event); } - get onDidRunOperation(): Event { return this._onDidRunOperation.event; } - fireAfterOperation(event: FileOperationEvent): void { this._onDidRunOperation.fire(event); } - resolve(resource: URI, _options?: IResolveFileOptions): Promise; + resolve(resource: URI, _options: IResolveMetadataFileOptions): Promise; + resolve(resource: URI, _options?: IResolveFileOptions): Promise; resolve(resource: URI, _options?: IResolveFileOptions): Promise { return Promise.resolve({ resource, @@ -850,6 +868,7 @@ export class TestFileService implements IFileService { isFile: true, isDirectory: false, isSymbolicLink: false, + readonly: this.readonly, name: basename(resource) }); } @@ -860,7 +879,7 @@ export class TestFileService implements IFileService { return stats.map(stat => ({ stat, success: true })); } - readonly notExistsSet = new Set(); + readonly notExistsSet = new ResourceMap(); async exists(_resource: URI): Promise { return !this.notExistsSet.has(_resource); } @@ -881,6 +900,7 @@ export class TestFileService implements IFileService { mtime: Date.now(), ctime: Date.now(), name: basename(resource), + readonly: this.readonly, size: 1 }); } @@ -900,6 +920,7 @@ export class TestFileService implements IFileService { mtime: Date.now(), ctime: Date.now(), size: 1, + readonly: this.readonly, name: basename(resource) }); } @@ -922,6 +943,7 @@ export class TestFileService implements IFileService { isFile: true, isDirectory: false, isSymbolicLink: false, + readonly: this.readonly, name: basename(resource) }); } @@ -958,7 +980,9 @@ export class TestFileService implements IFileService { return true; } - return false; + const provider = this.getProvider(resource.scheme); + + return !!(provider && (provider.capabilities & capability)); } async del(_resource: URI, _options?: { useTrash?: boolean, recursive?: boolean; }): Promise { } @@ -981,6 +1005,8 @@ export class TestFileService implements IFileService { export class TestWorkingCopyBackupService extends InMemoryWorkingCopyBackupService { + readonly resolved: Set = new Set(); + constructor() { super(); } @@ -992,6 +1018,12 @@ export class TestWorkingCopyBackupService extends InMemoryWorkingCopyBackupServi return textBuffer.getValueInRange(range, EndOfLinePreference.TextDefined); } + + override async resolve(identifier: IWorkingCopyIdentifier): Promise | undefined> { + this.resolved.add(identifier); + + return super.resolve(identifier); + } } export function toUntypedWorkingCopyId(resource: URI): IWorkingCopyIdentifier { @@ -1183,7 +1215,6 @@ export class TestInMemoryFileSystemProvider extends InMemoryFileSystemProvider i | FileSystemProviderCapabilities.PathCaseSensitive | FileSystemProviderCapabilities.FileReadStream; - readFileStream(resource: URI): ReadableStreamEvents { const BUFFER_SIZE = 64 * 1024; const stream = newWriteableStream(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer); @@ -1281,7 +1312,7 @@ export function registerTestEditor(id: string, inputs: SyncDescriptor { + override async setInput(input: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { super.setInput(input, options, context, token); await input.resolve(); @@ -1343,7 +1374,7 @@ export function registerTestFileEditor(): IDisposable { TestTextFileEditor.ID, 'Text File Editor' ), - [new SyncDescriptor(FileEditorInput)] + [new SyncDescriptor(FileEditorInput)] )); return disposables; @@ -1359,8 +1390,8 @@ export function registerTestResourceEditor(): IDisposable { 'Text Editor' ), [ - new SyncDescriptor(UntitledTextEditorInput), - new SyncDescriptor(ResourceEditorInput) + new SyncDescriptor(UntitledTextEditorInput), + new SyncDescriptor(TextResourceEditorInput) ] )); @@ -1403,6 +1434,16 @@ export class TestFileEditorInput extends EditorInput implements IFileEditorInput } override get typeId() { return this._typeId; } + + private _capabilities: EditorInputCapabilities = EditorInputCapabilities.None; + override get capabilities(): EditorInputCapabilities { return this._capabilities; } + override set capabilities(capabilities: EditorInputCapabilities) { + if (this._capabilities !== capabilities) { + this._capabilities = capabilities; + this._onDidChangeCapabilities.fire(); + } + } + override resolve(): Promise { return !this.fails ? Promise.resolve(null) : Promise.reject(new Error('fails')); } override matches(other: EditorInput): boolean { return !!(other?.resource && this.resource.toString() === other.resource.toString() && other instanceof TestFileEditorInput && other.typeId === this.typeId); } setPreferredResource(resource: URI): void { } @@ -1411,6 +1452,7 @@ export class TestFileEditorInput extends EditorInput implements IFileEditorInput setPreferredName(name: string): void { } setPreferredDescription(description: string): void { } setPreferredEncoding(encoding: string) { } + setPreferredContents(contents: string): void { } setMode(mode: string) { } setPreferredMode(mode: string) { } setForceOpenAsBinary(): void { } @@ -1436,9 +1478,6 @@ export class TestFileEditorInput extends EditorInput implements IFileEditorInput override isDirty(): boolean { return this.dirty; } - override isReadonly(): boolean { - return false; - } isResolved(): boolean { return false; } override dispose(): void { super.dispose(); @@ -1538,40 +1577,32 @@ export class TestWorkspacesService implements IWorkspacesService { async clearRecentlyOpened(): Promise { } async getRecentlyOpened(): Promise { return { files: [], workspaces: [] }; } async getDirtyWorkspaces(): Promise<(URI | IWorkspaceIdentifier)[]> { return []; } - async enterWorkspace(path: URI): Promise { throw new Error('Method not implemented.'); } + async enterWorkspace(path: URI): Promise { throw new Error('Method not implemented.'); } async getWorkspaceIdentifier(workspacePath: URI): Promise { throw new Error('Method not implemented.'); } } export class TestTerminalInstanceService implements ITerminalInstanceService { declare readonly _serviceBrand: undefined; - async getDefaultShellAndArgs(): Promise<{ shell: string, args: string[] | string | undefined }> { - return { - shell: 'bash', - args: undefined - }; - } - async getMainProcessParentEnv(): Promise { - return {}; - } - async getXtermConstructor(): Promise { throw new Error('Method not implemented.'); } async getXtermSearchConstructor(): Promise { throw new Error('Method not implemented.'); } async getXtermUnicode11Constructor(): Promise { throw new Error('Method not implemented.'); } async getXtermWebglConstructor(): Promise { throw new Error('Method not implemented.'); } - createWindowsShellHelper(shellProcessId: number, xterm: any): any { throw new Error('Method not implemented.'); } + preparePathForTerminalAsync(path: string, executable: string | undefined, title: string, shellType: TerminalShellType, isRemote: boolean): Promise { throw new Error('Method not implemented.'); } } export class TestTerminalProfileResolverService implements ITerminalProfileResolverService { _serviceBrand: undefined; + defaultProfileName = ''; resolveIcon(shellLaunchConfig: IShellLaunchConfig): void { } async resolveShellLaunchConfig(shellLaunchConfig: IShellLaunchConfig, options: IShellLaunchConfigResolveOptions): Promise { } - async getDefaultProfile(options: IShellLaunchConfigResolveOptions): Promise { return { path: '/default', profileName: 'Default' }; } + async getDefaultProfile(options: IShellLaunchConfigResolveOptions): Promise { return { path: '/default', profileName: 'Default', isDefault: true }; } async getDefaultShell(options: IShellLaunchConfigResolveOptions): Promise { return '/default'; } async getDefaultShellArgs(options: IShellLaunchConfigResolveOptions): Promise { return []; } - async getShellEnvironment(): Promise { return process.env; } + async getEnvironment(): Promise { return process.env; } getSafeConfigValue(key: string, os: OperatingSystem): unknown | undefined { return undefined; } getSafeConfigValueFullKey(key: string): unknown | undefined { return undefined; } + createProfileFromShellAndShellArgs(shell?: unknown, shellArgs?: unknown): Promise { throw new Error('Method not implemented.'); } } export class TestLocalTerminalService implements ILocalTerminalService { @@ -1588,11 +1619,16 @@ export class TestLocalTerminalService implements ILocalTerminalService { async attachToProcess(id: number): Promise { throw new Error('Method not implemented.'); } async listProcesses(): Promise { throw new Error('Method not implemented.'); } getDefaultSystemShell(osOverride?: OperatingSystem): Promise { throw new Error('Method not implemented.'); } - getShellEnvironment(): Promise { throw new Error('Method not implemented.'); } + getProfiles(isWorkspaceTrusted: boolean, includeDetectedProfiles?: boolean): Promise { throw new Error('Method not implemented.'); } + getEnvironment(): Promise { throw new Error('Method not implemented.'); } + getShellEnvironment(): Promise { throw new Error('Method not implemented.'); } + getWslPath(original: string): Promise { throw new Error('Method not implemented.'); } async setTerminalLayoutInfo(argsOrLayout?: ISetTerminalLayoutInfoArgs | ITerminalsLayoutInfoById) { throw new Error('Method not implemented.'); } async getTerminalLayoutInfo(): Promise { throw new Error('Method not implemented.'); } async reduceConnectionGraceTime(): Promise { throw new Error('Method not implemented.'); } processBinary(id: number, data: string): Promise { throw new Error('Method not implemented.'); } + updateTitle(id: number, title: string): Promise { throw new Error('Method not implemented.'); } + updateIcon(id: number, icon: URI | { light: URI; dark: URI } | { id: string, color?: { id: string } }, color?: string): Promise { throw new Error('Method not implemented.'); } } class TestTerminalChildProcess implements ITerminalChildProcess { @@ -1650,3 +1686,18 @@ export class TestQuickInputService implements IQuickInputService { back(): Promise { throw new Error('not implemented.'); } cancel(): Promise { throw new Error('not implemented.'); } } + +export class TestEditorWorkerService implements IEditorWorkerService { + + declare readonly _serviceBrand: undefined; + + canComputeDiff(original: URI, modified: URI): boolean { return false; } + async computeDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean, maxComputationTime: number): Promise { return null; } + canComputeDirtyDiff(original: URI, modified: URI): boolean { return false; } + async computeDirtyDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean): Promise { return null; } + async computeMoreMinimalEdits(resource: URI, edits: TextEdit[] | null | undefined): Promise { return undefined; } + canComputeWordRanges(resource: URI): boolean { return false; } + async computeWordRanges(resource: URI, range: IRange): Promise<{ [word: string]: IRange[]; } | null> { return null; } + canNavigateValueSet(resource: URI): boolean { return false; } + async navigateValueSet(resource: URI, range: IRange, up: boolean): Promise { return null; } +} diff --git a/src/vs/workbench/test/common/notifications.test.ts b/src/vs/workbench/test/common/notifications.test.ts index c72b472aff..39df708b5e 100644 --- a/src/vs/workbench/test/common/notifications.test.ts +++ b/src/vs/workbench/test/common/notifications.test.ts @@ -10,6 +10,7 @@ import { INotification, Severity, NotificationsFilter } from 'vs/platform/notifi import { createErrorWithActions } from 'vs/base/common/errors'; import { NotificationService } from 'vs/workbench/services/notification/common/notificationService'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; +import { timeout } from 'vs/base/common/async'; suite('Notifications', () => { @@ -143,6 +144,23 @@ suite('Notifications', () => { assert.strictEqual(item11.silent, true); }); + test('Items - does not fire changed when message did not change (content, severity)', async () => { + const item1 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message' })!; + + let fired = false; + item1.onDidChangeContent(() => { + fired = true; + }); + + item1.updateMessage('Error Message'); + await timeout(0); + assert.ok(!fired, 'Expected onDidChangeContent to not be fired'); + + item1.updateSeverity(Severity.Error); + await timeout(0); + assert.ok(!fired, 'Expected onDidChangeContent to not be fired'); + }); + test('Model', () => { const model = new NotificationsModel(); @@ -167,11 +185,11 @@ suite('Notifications', () => { assert.strictEqual(lastNotificationEvent.index, 0); assert.strictEqual(lastNotificationEvent.kind, NotificationChangeType.ADD); - item1Handle.updateMessage('Error Message'); + item1Handle.updateMessage('Different Error Message'); assert.strictEqual(lastNotificationEvent.kind, NotificationChangeType.CHANGE); assert.strictEqual(lastNotificationEvent.detail, NotificationViewItemContentChangeKind.MESSAGE); - item1Handle.updateSeverity(Severity.Error); + item1Handle.updateSeverity(Severity.Warning); assert.strictEqual(lastNotificationEvent.kind, NotificationChangeType.CHANGE); assert.strictEqual(lastNotificationEvent.detail, NotificationViewItemContentChangeKind.SEVERITY); @@ -205,8 +223,8 @@ suite('Notifications', () => { item1Handle.close(); assert.strictEqual(called, 1); assert.strictEqual(model.notifications.length, 2); - assert.strictEqual(lastNotificationEvent.item.severity, item1.severity); - assert.strictEqual(lastNotificationEvent.item.message.linkedText.toString(), item1.message); + assert.strictEqual(lastNotificationEvent.item.severity, Severity.Warning); + assert.strictEqual(lastNotificationEvent.item.message.linkedText.toString(), 'Different Error Message'); assert.strictEqual(lastNotificationEvent.index, 2); assert.strictEqual(lastNotificationEvent.kind, NotificationChangeType.REMOVE); diff --git a/src/vs/workbench/test/common/workbenchTestServices.ts b/src/vs/workbench/test/common/workbenchTestServices.ts index abc221c71c..aef5d91ba8 100644 --- a/src/vs/workbench/test/common/workbenchTestServices.ts +++ b/src/vs/workbench/test/common/workbenchTestServices.ts @@ -145,7 +145,7 @@ export class TestWorkingCopy extends Disposable implements IWorkingCopy { private dirty = false; - constructor(public readonly resource: URI, isDirty = false, public readonly typeId = 'testWorkingCopyType') { + constructor(readonly resource: URI, isDirty = false, readonly typeId = 'testWorkingCopyType') { super(); this.dirty = isDirty; diff --git a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts index 9f801f0df0..0ed7302aab 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts @@ -883,14 +883,16 @@ suite('ExtHostSearch', () => { }); test('basic sibling clause', async () => { - mockPFS.readdir = (_path: string): any => { - if (_path === rootFolderA.fsPath) { - return Promise.resolve([ - 'file1.js', - 'file1.ts' - ]); - } else { - return Promise.reject(new Error('Wrong path')); + (mockPFS as any).Promises = { + readdir: (_path: string): any => { + if (_path === rootFolderA.fsPath) { + return Promise.resolve([ + 'file1.js', + 'file1.ts' + ]); + } else { + return Promise.reject(new Error('Wrong path')); + } } }; @@ -926,21 +928,23 @@ suite('ExtHostSearch', () => { }); test('multiroot sibling clause', async () => { - mockPFS.readdir = (_path: string): any => { - if (_path === joinPath(rootFolderA, 'folder').fsPath) { - return Promise.resolve([ - 'fileA.scss', - 'fileA.css', - 'file2.css' - ]); - } else if (_path === rootFolderB.fsPath) { - return Promise.resolve([ - 'fileB.ts', - 'fileB.js', - 'file3.js' - ]); - } else { - return Promise.reject(new Error('Wrong path')); + (mockPFS as any).Promises = { + readdir: (_path: string): any => { + if (_path === joinPath(rootFolderA, 'folder').fsPath) { + return Promise.resolve([ + 'fileA.scss', + 'fileA.css', + 'file2.css' + ]); + } else if (_path === rootFolderB.fsPath) { + return Promise.resolve([ + 'fileB.ts', + 'fileB.js', + 'file3.js' + ]); + } else { + return Promise.reject(new Error('Wrong path')); + } } }; diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadWorkspace.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadWorkspace.test.ts index 4920d55211..05de65e1d3 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadWorkspace.test.ts +++ b/src/vs/workbench/test/electron-browser/api/mainThreadWorkspace.test.ts @@ -31,7 +31,7 @@ suite('MainThreadWorkspace', () => { assert.strictEqual(query.folderQueries.length, 1); assert.strictEqual(query.folderQueries[0].disregardIgnoreFiles, true); - assert.deepEqual(query.includePattern, { 'foo': true }); + assert.deepStrictEqual({ ...query.includePattern }, { 'foo': true }); assert.strictEqual(query.maxResults, 10); return Promise.resolve({ results: [], messages: [] }); @@ -89,7 +89,7 @@ suite('MainThreadWorkspace', () => { instantiationService.stub(ISearchService, { fileSearch(query: IFileQuery) { assert.strictEqual(query.folderQueries[0].excludePattern, undefined); - assert.deepEqual(query.excludePattern, { 'exclude/**': true }); + assert.deepStrictEqual({ ...query.excludePattern }, { 'exclude/**': true }); return Promise.resolve({ results: [], messages: [] }); } diff --git a/src/vs/workbench/test/electron-browser/colorRegistry.releaseTest.ts b/src/vs/workbench/test/electron-browser/colorRegistry.releaseTest.ts index 447e14fefb..178ea4fb42 100644 --- a/src/vs/workbench/test/electron-browser/colorRegistry.releaseTest.ts +++ b/src/vs/workbench/test/electron-browser/colorRegistry.releaseTest.ts @@ -6,7 +6,6 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IColorRegistry, Extensions, ColorContribution } from 'vs/platform/theme/common/colorRegistry'; import { asText } from 'vs/platform/request/common/request'; -import * as fs from 'fs'; import * as pfs from 'vs/base/node/pfs'; import * as path from 'vs/base/common/path'; import * as assert from 'assert'; @@ -102,11 +101,11 @@ function getDescription(color: ColorContribution) { async function getColorsFromExtension(): Promise<{ [id: string]: string }> { let extPath = getPathFromAmdModule(require, '../../../../../extensions'); - let extFolders = await pfs.readDirsInDir(extPath); + let extFolders = await pfs.Promises.readDirsInDir(extPath); let result: { [id: string]: string } = Object.create(null); for (let folder of extFolders) { try { - let packageJSON = JSON.parse((await fs.promises.readFile(path.join(extPath, folder, 'package.json'))).toString()); + let packageJSON = JSON.parse((await pfs.Promises.readFile(path.join(extPath, folder, 'package.json'))).toString()); let contributes = packageJSON['contributes']; if (contributes) { let colors = contributes['colors']; diff --git a/src/vs/workbench/test/electron-browser/colorRegistryExport.test.ts b/src/vs/workbench/test/electron-browser/colorRegistryExport.test.ts new file mode 100644 index 0000000000..c8d9e7e2d3 --- /dev/null +++ b/src/vs/workbench/test/electron-browser/colorRegistryExport.test.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ + +import { Color } from 'vs/base/common/color'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions, IColorRegistry } from 'vs/platform/theme/common/colorRegistry'; + +suite('ColorRegistry', () => { + if (process.env.VSCODE_COLOR_REGISTRY_EXPORT) { + test('exports', () => { + const themingRegistry = Registry.as(Extensions.ColorContribution); + const colors = themingRegistry.getColors(); + const replacer = (_key: string, value: unknown) => + value instanceof Color ? Color.Format.CSS.formatHexA(value) : value; + console.log(`#colors:${JSON.stringify(colors, replacer)}\n`); + }); + } +}); diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index 37d9780fc0..a4aaae2a3a 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -23,7 +23,7 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService import { URI } from 'vs/base/common/uri'; import { IReadTextFileOptions, ITextFileStreamContent, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; -import { IOpenEmptyWindowOptions, IWindowOpenable, IOpenWindowOptions, IOpenedWindow } from 'vs/platform/windows/common/windows'; +import { IOpenEmptyWindowOptions, IWindowOpenable, IOpenWindowOptions, IOpenedWindow, IPartsSplash } from 'vs/platform/windows/common/windows'; import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; import { LogLevel, ILogService } from 'vs/platform/log/common/log'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; @@ -53,7 +53,6 @@ export const TestWorkbenchConfiguration: INativeWorkbenchConfiguration = { machineId: 'testMachineId', logLevel: LogLevel.Error, mainPid: 0, - partsSplashPath: '', appRoot: '', userEnv: {}, execPath: process.execPath, @@ -136,7 +135,8 @@ export class TestTextFileService extends NativeTextFileService { etag: content.etag, encoding: 'utf8', value: await createTextBufferFactoryFromStream(content.value), - size: 10 + size: 10, + readonly: false }; } } @@ -197,6 +197,7 @@ export class TestNativeHostService implements INativeHostService { async unmaximizeWindow(): Promise { } async minimizeWindow(): Promise { } async setMinimumSize(width: number | undefined, height: number | undefined): Promise { } + async saveWindowSplash(value: IPartsSplash): Promise { } async focusWindow(options?: { windowId?: number | undefined; } | undefined): Promise { } async showMessageBox(options: Electron.MessageBoxOptions): Promise { throw new Error('Method not implemented.'); } async showSaveDialog(options: Electron.SaveDialogOptions): Promise { throw new Error('Method not implemented.'); } @@ -216,13 +217,15 @@ export class TestNativeHostService implements INativeHostService { async setDocumentEdited(edited: boolean): Promise { } async openExternal(url: string): Promise { return false; } async updateTouchBar(): Promise { } - async moveItemToTrash(): Promise { return false; } + async moveItemToTrash(): Promise { } async newWindowTab(): Promise { } async showPreviousWindowTab(): Promise { } async showNextWindowTab(): Promise { } async moveWindowTabToNewWindow(): Promise { } async mergeAllWindowTabs(): Promise { } async toggleWindowTabsBar(): Promise { } + async installShellCommand(): Promise { } + async uninstallShellCommand(): Promise { } async notifyReady(): Promise { } async relaunch(options?: { addArgs?: string[] | undefined; removeArgs?: string[] | undefined; } | undefined): Promise { } async reload(): Promise { } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 66eb51fafe..5a15996474 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -45,6 +45,7 @@ import 'vs/workbench/browser/parts/editor/editor.contribution'; import 'vs/workbench/browser/parts/editor/editorPart'; import 'vs/workbench/browser/parts/activitybar/activitybarPart'; import 'vs/workbench/browser/parts/panel/panelPart'; +import 'vs/workbench/browser/parts/banner/bannerPart'; import 'vs/workbench/browser/parts/sidebar/sidebarPart'; import 'vs/workbench/browser/parts/statusbar/statusbarPart'; import 'vs/workbench/browser/parts/views/viewsService'; @@ -340,6 +341,7 @@ import 'vs/workbench/contrib/output/browser/outputView'; // Terminal import 'vs/workbench/contrib/terminal/common/environmentVariable.contribution'; import 'vs/workbench/contrib/terminal/common/terminalExtensionPoints.contribution'; +import 'vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution'; import 'vs/workbench/contrib/terminal/browser/terminal.contribution'; import 'vs/workbench/contrib/terminal/browser/terminalView'; @@ -362,9 +364,6 @@ import 'vs/workbench/contrib/codeEditor/browser/codeEditor.contribution'; // Keybindings Contributions import 'vs/workbench/contrib/keybindings/browser/keybindings.contribution'; -// Execution -import 'vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution'; - // Snippets import 'vs/workbench/contrib/snippets/browser/snippets.contribution'; import 'vs/workbench/contrib/snippets/browser/snippetsService'; diff --git a/src/vs/workbench/workbench.desktop.main.css b/src/vs/workbench/workbench.desktop.main.css index c8343db9c5..04a7af7a08 100644 --- a/src/vs/workbench/workbench.desktop.main.css +++ b/src/vs/workbench/workbench.desktop.main.css @@ -6,4 +6,4 @@ /* NOTE: THIS FILE WILL BE OVERWRITTEN DURING BUILD TIME, DO NOT EDIT */ div.monaco.main.css { -} \ No newline at end of file +} diff --git a/src/vs/workbench/workbench.desktop.main.nls.js b/src/vs/workbench/workbench.desktop.main.nls.js index 4d6dbb92d5..28f20a5e93 100644 --- a/src/vs/workbench/workbench.desktop.main.nls.js +++ b/src/vs/workbench/workbench.desktop.main.nls.js @@ -5,4 +5,4 @@ // NOTE: THIS FILE WILL BE OVERWRITTEN DURING BUILD TIME, DO NOT EDIT -define([], {}); \ No newline at end of file +define([], {}); diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 73fd20418e..588470a2c2 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -104,9 +104,6 @@ registerSingleton(IQueryHistoryService, QueryHistoryService); //#region --- workbench contributions -// Rapid Render Splash -import 'vs/workbench/contrib/splash/electron-browser/partsSplash.contribution'; - // Webview import 'vs/workbench/contrib/webview/electron-browser/webview.contribution'; @@ -127,27 +124,6 @@ import 'vs/workbench/contrib/webview/electron-browser/webview.contribution'; // Extensions Management import 'vs/workbench/contrib/extensions/electron-browser/extensions.contribution'; -// Terminal -import 'vs/workbench/contrib/terminal/electron-browser/terminal.contribution'; - -// External Terminal -import 'vs/workbench/contrib/externalTerminal/node/externalTerminal.contribution'; - - -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -// -// NOTE: Please do NOT register services here. Use `registerSingleton()` -// from `workbench.common.main.ts` if the service is shared between -// desktop and web or `workbench.sandbox.main.ts` if the service -// is desktop only. -// -// The `node` & `electron-browser` layer is deprecated for workbench! -// -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - - -// CLI -import 'vs/workbench/contrib/cli/node/cli.contribution'; // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! diff --git a/src/vs/workbench/workbench.desktop.sandbox.main.ts b/src/vs/workbench/workbench.desktop.sandbox.main.ts index f9f0108b04..a26ffb2685 100644 --- a/src/vs/workbench/workbench.desktop.sandbox.main.ts +++ b/src/vs/workbench/workbench.desktop.sandbox.main.ts @@ -39,5 +39,7 @@ import 'vs/workbench/electron-sandbox/desktop.main'; //#region --- workbench contributions +// Webview (using the iframe based solution) +import 'vs/workbench/contrib/webview/browser/webview.web.contribution'; //#endregion diff --git a/src/vs/workbench/workbench.sandbox.main.ts b/src/vs/workbench/workbench.sandbox.main.ts index d760bfe289..18987bd64e 100644 --- a/src/vs/workbench/workbench.sandbox.main.ts +++ b/src/vs/workbench/workbench.sandbox.main.ts @@ -101,6 +101,9 @@ import 'vs/workbench/contrib/codeEditor/electron-sandbox/codeEditor.contribution // Debug import 'vs/workbench/contrib/debug/electron-sandbox/extensionHostDebugService'; +// Extensions Management +import 'vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution'; + // Telemetry Opt Out import 'vs/workbench/contrib/welcome/telemetryOptOut/electron-sandbox/telemetryOptOut.contribution'; @@ -135,4 +138,7 @@ import 'vs/workbench/contrib/performance/electron-sandbox/performance.contributi // Tasks import 'vs/workbench/contrib/tasks/electron-sandbox/taskService'; +// External terminal +import 'vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution'; + //#endregion diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index 33c539479b..6c23defe44 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -321,6 +321,11 @@ interface IWorkbenchConstructionOptions { */ readonly staticExtensions?: readonly IStaticExtension[]; + /** + * Filter for built-in extensions. + */ + readonly builtinExtensionsFilter?: (extensionId: string) => boolean; + /** * [TEMPORARY]: This will be removed soon. * Enable inlined extensions. @@ -328,6 +333,13 @@ interface IWorkbenchConstructionOptions { */ readonly _enableBuiltinExtensions?: boolean; + /** + * Allows the workbench to skip checking whether an extension was built for the web + * and assumes they are addressable via the `Microsoft.VisualStudio.Code.WebResources` + * asset URI. + */ + readonly assumeGalleryExtensionsAreAddressable?: boolean; + /** * Support for URL callbacks. */ @@ -457,10 +469,12 @@ interface IWorkbench { } env: { + readonly uriScheme: string; /** * @see [retrievePerformanceMarks](#commands.retrievePerformanceMarks) */ retrievePerformanceMarks(): Promise<[string, readonly IPerformanceMark[]][]>; + openUri(uri: URI): Promise; } /** @@ -560,6 +574,16 @@ namespace env { return workbench.env.retrievePerformanceMarks(); } + + export async function getUriScheme(): Promise { + const workbench = await workbenchPromise; + return workbench.env.uriScheme; + } + + export async function openUri(target: URI): Promise { + const workbench = await workbenchPromise; + return workbench.env.openUri(target); + } } export { diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 8bd65f90a6..92df2c8d64 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -138,6 +138,7 @@ import 'vs/workbench/contrib/extensions/browser/extensions.web.contribution'; // Terminal import 'vs/workbench/contrib/terminal/browser/terminal.web.contribution'; +import 'vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution'; import 'vs/workbench/contrib/terminal/browser/terminalInstanceService'; // Tasks diff --git a/test/automation/package.json b/test/automation/package.json index edad2f731a..823dfa5027 100644 --- a/test/automation/package.json +++ b/test/automation/package.json @@ -23,7 +23,7 @@ "@types/debug": "4.1.5", "@types/mkdirp": "^1.0.1", "@types/ncp": "2.0.1", - "@types/node": "^12.19.9", + "@types/node": "14.x", "@types/tmp": "0.1.0", "cpx2": "3.0.0", "mkdirp": "^1.0.4", diff --git a/test/automation/src/application.ts b/test/automation/src/application.ts index 2264ed9e68..a243462308 100644 --- a/test/automation/src/application.ts +++ b/test/automation/src/application.ts @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. + * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; diff --git a/test/automation/src/code.ts b/test/automation/src/code.ts index 209c4a8cb7..f9fe7b2490 100644 --- a/test/automation/src/code.ts +++ b/test/automation/src/code.ts @@ -140,6 +140,7 @@ export async function spawn(options: SpawnOptions): Promise { '--disable-updates', '--disable-keytar', '--disable-crash-reporter', + '--disable-workspace-trust', `--extensions-dir=${options.extensionsPath}`, `--user-data-dir=${options.userDataDir}`, '--driver', handle @@ -310,9 +311,9 @@ export class Code { return element.textContent; } - async waitAndClick(selector: string, xoffset?: number, yoffset?: number): Promise { + async waitAndClick(selector: string, xoffset?: number, yoffset?: number, retryCount: number = 200): Promise { const windowId = await this.getActiveWindowId(); - await poll(() => this.driver.click(windowId, selector, xoffset, yoffset), () => true, `click '${selector}'`); + await poll(() => this.driver.click(windowId, selector, xoffset, yoffset), () => true, `click '${selector}'`, retryCount); } async waitAndDoubleClick(selector: string): Promise { diff --git a/test/automation/src/driver.js b/test/automation/src/driver.js index 2976c1d5bb..eb8971ac22 100644 --- a/test/automation/src/driver.js +++ b/test/automation/src/driver.js @@ -9,4 +9,4 @@ exports.connect = function (outPath, handle) { const bootstrapPath = path.join(outPath, 'bootstrap-amd.js'); const { load } = require(bootstrapPath); return new Promise((c, e) => load('vs/platform/driver/node/driver', ({ connect }) => connect(handle).then(c, e), e)); -}; \ No newline at end of file +}; diff --git a/test/automation/src/logger.ts b/test/automation/src/logger.ts index 909395a8b5..0e49342a59 100644 --- a/test/automation/src/logger.ts +++ b/test/automation/src/logger.ts @@ -39,4 +39,4 @@ export class MultiLogger implements Logger { logger.log(message, ...args); } } -} \ No newline at end of file +} diff --git a/test/automation/src/playwrightDriver.ts b/test/automation/src/playwrightDriver.ts index 128554b4be..951c644313 100644 --- a/test/automation/src/playwrightDriver.ts +++ b/test/automation/src/playwrightDriver.ts @@ -35,6 +35,7 @@ function buildDriver(browser: playwright.Browser, page: playwright.Page): IDrive getWindowIds: () => { return Promise.resolve([1]); }, + // {{SQL CARBON EDIT}} capturePage: async () => { const buffer = await page.screenshot(); return buffer.toString('base64'); diff --git a/test/automation/src/search.ts b/test/automation/src/search.ts index cef769de01..918af386ef 100644 --- a/test/automation/src/search.ts +++ b/test/automation/src/search.ts @@ -43,6 +43,11 @@ export class Search extends Viewlet { await this.waitForInputFocus(INPUT); } + async getSearchTooltip(): Promise { + const icon = await this.code.waitForElement(`.activitybar .action-label.codicon.codicon-search-view-icon`, (el) => !!el?.attributes?.['title']); + return icon.attributes['title']; + } + async searchFor(text: string): Promise { await this.waitForInputFocus(INPUT); await this.code.waitForSetValue(INPUT, text); diff --git a/test/automation/yarn.lock b/test/automation/yarn.lock index f3458adaef..71d52ecb42 100644 --- a/test/automation/yarn.lock +++ b/test/automation/yarn.lock @@ -26,10 +26,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.1.tgz#3b5c3a26393c19b400844ac422bd0f631a94d69d" integrity sha512-aK9jxMypeSrhiYofWWBf/T7O+KwaiAHzM4sveCdWPn71lzUSMimRnKzhXDKfKwV1kWoBo2P1aGgaIYGLf9/ljw== -"@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@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== "@types/tmp@0.1.0": version "0.1.0" diff --git a/test/integration/browser/package.json b/test/integration/browser/package.json index d29777d395..90be82a546 100644 --- a/test/integration/browser/package.json +++ b/test/integration/browser/package.json @@ -8,7 +8,7 @@ }, "devDependencies": { "@types/mkdirp": "^1.0.1", - "@types/node": "^12.19.9", + "@types/node": "14.x", "@types/optimist": "0.0.29", "@types/rimraf": "^2.0.4", "@types/tmp": "0.1.0", diff --git a/test/integration/browser/src/index.ts b/test/integration/browser/src/index.ts index 15f46ce620..252d14aa30 100644 --- a/test/integration/browser/src/index.ts +++ b/test/integration/browser/src/index.ts @@ -32,7 +32,7 @@ const height = 800; type BrowserType = 'chromium' | 'firefox' | 'webkit'; async function runTestsInBrowser(browserType: BrowserType, endpoint: url.UrlWithStringQuery, server: cp.ChildProcess): Promise { - const args = process.platform === 'linux' && browserType === 'chromium' ? ['--no-sandbox'] : undefined; // disable sandbox to run chrome on certain Linux distros + const args = process.platform === 'linux' && browserType === 'chromium' ? ['--disable-setuid-sandbox'] : undefined; // setuid sandboxes requires root and is used in containers so we disable this to support our CI const browser = await playwright[browserType].launch({ headless: !Boolean(optimist.argv.debug), args }); const context = await browser.newContext(); const page = await context.newPage(); @@ -46,7 +46,7 @@ async function runTestsInBrowser(browserType: BrowserType, endpoint: url.UrlWith const testFilesUri = url.format({ pathname: URI.file(path.resolve(optimist.argv.extensionTestsPath)).path, protocol, host, slashes: true }); const folderParam = testWorkspaceUri; - const payloadParam = `[["extensionDevelopmentPath","${testExtensionUri}"],["extensionTestsPath","${testFilesUri}"],["enableProposedApi",""]]`; + const payloadParam = `[["extensionDevelopmentPath","${testExtensionUri}"],["extensionTestsPath","${testFilesUri}"],["enableProposedApi",""],["webviewExternalEndpointCommit","5319757634f77a050b49c10162939bfe60970c29"]]`; await page.goto(`${endpoint.href}&folder=${folderParam}&payload=${payloadParam}`); diff --git a/test/integration/browser/yarn.lock b/test/integration/browser/yarn.lock index 8cdd9ff744..3adb041363 100644 --- a/test/integration/browser/yarn.lock +++ b/test/integration/browser/yarn.lock @@ -33,10 +33,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.0.tgz#b417deda18cf8400f278733499ad5547ed1abec4" integrity sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ== -"@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@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== "@types/optimist@0.0.29": version "0.0.29" diff --git a/test/leaks/index.html b/test/leaks/index.html new file mode 100644 index 0000000000..7e1914a3b5 --- /dev/null +++ b/test/leaks/index.html @@ -0,0 +1,40 @@ + + + + + Leak Test Bed + + + + + + + + + + + diff --git a/test/leaks/package.json b/test/leaks/package.json new file mode 100644 index 0000000000..07bdd1d183 --- /dev/null +++ b/test/leaks/package.json @@ -0,0 +1,11 @@ +{ + "name": "leaks", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "devDependencies": { + "koa": "^2.13.1", + "koa-mount": "^4.0.0", + "koa-static": "^5.0.0" + } +} diff --git a/test/leaks/server.js b/test/leaks/server.js new file mode 100644 index 0000000000..7167dc72c9 --- /dev/null +++ b/test/leaks/server.js @@ -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. + *--------------------------------------------------------------------------------------------*/ + +const Koa = require('koa'); +const serve = require('koa-static'); +const mount = require('koa-mount'); + +const app = new Koa(); + +app.use(serve('.')); +app.use(mount('/static', serve('../../out'))); + +app.listen(3000); +console.log('👉 http://localhost:3000'); diff --git a/test/leaks/yarn.lock b/test/leaks/yarn.lock new file mode 100644 index 0000000000..d1fa87793a --- /dev/null +++ b/test/leaks/yarn.lock @@ -0,0 +1,371 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +accepts@^1.3.5: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +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= + +cache-content-type@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c" + integrity sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA== + dependencies: + mime-types "^2.1.18" + ylru "^1.2.0" + +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= + +content-disposition@~0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookies@~0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90" + integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow== + dependencies: + depd "~2.0.0" + keygrip "~1.1.0" + +debug@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@^4.0.1, debug@^4.1.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" + +debug@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +deep-equal@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +depd@^2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +destroy@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +encodeurl@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +escape-html@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +fresh@~0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +http-assert@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.4.1.tgz#c5f725d677aa7e873ef736199b89686cceb37878" + integrity sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw== + dependencies: + deep-equal "~1.0.1" + http-errors "~1.7.2" + +http-errors@^1.6.3, http-errors@^1.7.3: + version "1.8.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz#75d1bbe497e1044f51e4ee9e704a62f28d336507" + integrity sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-generator-function@^1.0.7: + version "1.0.9" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.9.tgz#e5f82c2323673e7fcad3d12858c83c4039f6399c" + integrity sha512-ZJ34p1uvIfptHCN7sFTjGibB9/oBg17sHqzDLfuwhvmN/qLVvIQXRQ8licZQ35WJ8KuEQt/etnnzQFI9C9Ue/A== + +keygrip@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226" + integrity sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ== + dependencies: + tsscmp "1.0.6" + +koa-compose@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7" + integrity sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec= + dependencies: + any-promise "^1.1.0" + +koa-compose@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877" + integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw== + +koa-convert@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0" + integrity sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA= + dependencies: + co "^4.6.0" + koa-compose "^3.0.0" + +koa-mount@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/koa-mount/-/koa-mount-4.0.0.tgz#e0265e58198e1a14ef889514c607254ff386329c" + integrity sha512-rm71jaA/P+6HeCpoRhmCv8KVBIi0tfGuO/dMKicbQnQW/YJntJ6MnnspkodoA4QstMVEZArsCphmd0bJEtoMjQ== + dependencies: + debug "^4.0.1" + koa-compose "^4.1.0" + +koa-send@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/koa-send/-/koa-send-5.0.1.tgz#39dceebfafb395d0d60beaffba3a70b4f543fe79" + integrity sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ== + dependencies: + debug "^4.1.1" + http-errors "^1.7.3" + resolve-path "^1.4.0" + +koa-static@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/koa-static/-/koa-static-5.0.0.tgz#5e92fc96b537ad5219f425319c95b64772776943" + integrity sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ== + dependencies: + debug "^3.1.0" + koa-send "^5.0.0" + +koa@^2.13.1: + version "2.13.1" + resolved "https://registry.yarnpkg.com/koa/-/koa-2.13.1.tgz#6275172875b27bcfe1d454356a5b6b9f5a9b1051" + integrity sha512-Lb2Dloc72auj5vK4X4qqL7B5jyDPQaZucc9sR/71byg7ryoD1NCaCm63CShk9ID9quQvDEi1bGR/iGjCG7As3w== + dependencies: + accepts "^1.3.5" + cache-content-type "^1.0.0" + content-disposition "~0.5.2" + content-type "^1.0.4" + cookies "~0.8.0" + debug "~3.1.0" + delegates "^1.0.0" + depd "^2.0.0" + destroy "^1.0.4" + encodeurl "^1.0.2" + escape-html "^1.0.3" + fresh "~0.5.2" + http-assert "^1.3.0" + http-errors "^1.6.3" + is-generator-function "^1.0.7" + koa-compose "^4.1.0" + koa-convert "^1.2.0" + on-finished "^2.3.0" + only "~0.0.2" + parseurl "^1.3.2" + statuses "^1.5.0" + type-is "^1.6.16" + vary "^1.1.2" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +mime-db@1.47.0: + version "1.47.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.47.0.tgz#8cb313e59965d3c05cfbf898915a267af46a335c" + integrity sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw== + +mime-types@^2.1.18, mime-types@~2.1.24: + version "2.1.30" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.30.tgz#6e7be8b4c479825f85ed6326695db73f9305d62d" + integrity sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg== + dependencies: + mime-db "1.47.0" + +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: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +on-finished@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +only@~0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" + integrity sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q= + +parseurl@^1.3.2: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-is-absolute@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +resolve-path@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/resolve-path/-/resolve-path-1.4.0.tgz#c4bda9f5efb2fce65247873ab36bb4d834fe16f7" + integrity sha1-xL2p9e+y/OZSR4c6s2u02DT+Fvc= + dependencies: + http-errors "~1.6.2" + path-is-absolute "1.0.1" + +safe-buffer@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +tsscmp@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" + integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA== + +type-is@^1.6.16: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +vary@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +ylru@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.2.1.tgz#f576b63341547989c1de7ba288760923b27fe84f" + integrity sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ== diff --git a/test/smoke/package.json b/test/smoke/package.json index cc680c4eb2..cdd563b189 100644 --- a/test/smoke/package.json +++ b/test/smoke/package.json @@ -15,7 +15,7 @@ "@types/mkdirp": "^1.0.1", "@types/mocha": "^8.2.0", "@types/ncp": "2.0.1", - "@types/node": "^12.19.9", + "@types/node": "14.x", "@types/rimraf": "^2.0.4", "@types/tmp": "0.0.33", "cpx": "^1.5.0", diff --git a/test/smoke/src/areas/multiroot/multiroot.test.ts b/test/smoke/src/areas/multiroot/multiroot.test.ts index 4def1955a4..7e717f33ff 100644 --- a/test/smoke/src/areas/multiroot/multiroot.test.ts +++ b/test/smoke/src/areas/multiroot/multiroot.test.ts @@ -22,7 +22,11 @@ async function createWorkspaceFile(workspacePath: string): Promise { { path: toUri(path.join(workspacePath, 'public')) }, { path: toUri(path.join(workspacePath, 'routes')) }, { path: toUri(path.join(workspacePath, 'views')) } - ] + ], + settings: { + 'workbench.startupEditor': 'none', + 'workbench.enableExperiments': false + } }; fs.writeFileSync(workspaceFilePath, JSON.stringify(workspace, null, '\t')); diff --git a/test/smoke/src/areas/notebook/notebook.test.ts b/test/smoke/src/areas/notebook/notebook.test.ts index 9942f94307..3db3641d44 100644 --- a/test/smoke/src/areas/notebook/notebook.test.ts +++ b/test/smoke/src/areas/notebook/notebook.test.ts @@ -63,7 +63,7 @@ export function setup() { await app.workbench.notebook.waitForActiveCellEditorContents('code()'); }); - it('cell action execution', async function () { + it.skip('cell action execution', async function () { const app = this.app as Application; await app.workbench.notebook.openNotebook(); await app.workbench.notebook.insertNotebookCell('code'); diff --git a/test/smoke/src/areas/search/search.test.ts b/test/smoke/src/areas/search/search.test.ts index 98717191ed..d5bc56ac8a 100644 --- a/test/smoke/src/areas/search/search.test.ts +++ b/test/smoke/src/areas/search/search.test.ts @@ -15,6 +15,15 @@ export function setup() { cp.execSync('git reset --hard HEAD --quiet', { cwd: app.workspacePathOrFolder }); }); + // https://github.com/microsoft/vscode/issues/124146 + it.skip /* https://github.com/microsoft/vscode/issues/124335 */('has a tooltp with a keybinding', async function () { + const app = this.app as Application; + const tooltip: string = await app.workbench.search.getSearchTooltip(); + if (!/Search \(.+\)/.test(tooltip)) { + throw Error(`Expected search tooltip to contain keybinding but got ${tooltip}`); + } + }); + it('searches for body & checks for correct result number', async function () { const app = this.app as Application; await app.workbench.search.openSearchViewlet(); diff --git a/test/smoke/src/areas/workbench/localization.test.ts b/test/smoke/src/areas/workbench/localization.test.ts index 968db993f3..ef33ee3d76 100644 --- a/test/smoke/src/areas/workbench/localization.test.ts +++ b/test/smoke/src/areas/workbench/localization.test.ts @@ -10,7 +10,8 @@ export function setup() { before(async function () { const app = this.app as Application; - if (app.quality === Quality.Dev) { + // Don't run the localization tests in dev or remote. + if (app.quality === Quality.Dev || app.remote) { return; } @@ -23,7 +24,7 @@ export function setup() { it(`starts with 'DE' locale and verifies title and viewlets text is in German`, async function () { const app = this.app as Application; - if (app.quality === Quality.Dev) { + if (app.quality === Quality.Dev || app.remote) { this.skip(); return; } diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index fe172cb08d..ae7620b743 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -77,6 +77,7 @@ if (screenshotsPath) { mkdirp.sync(screenshotsPath); } +// {{SQL CARBON EDIT}} Add logs to smoke tests const logPath = opts.log ? path.resolve(opts.log) : null; if (logPath) { mkdirp.sync(path.dirname(logPath)); @@ -220,6 +221,7 @@ async function setupRepository(): Promise { cp.spawnSync('git', ['clean', '-xdf'], { cwd: workspacePath }); } + // None of the test run the project // console.log('*** Running yarn...'); // cp.execSync('yarn', { cwd: workspacePath, stdio: 'inherit' }); } @@ -268,7 +270,7 @@ before(async function () { this.timeout(2 * 60 * 1000); // allow two minutes for setup await setup(); this.defaultOptions = createOptions(); - await sqlSetup(this.defaultOptions); + await sqlSetup(this.defaultOptions); // {{SQL CARBON EDIT}} }); after(async function () { diff --git a/test/smoke/yarn.lock b/test/smoke/yarn.lock index 6978a5bd57..72e50c029b 100644 --- a/test/smoke/yarn.lock +++ b/test/smoke/yarn.lock @@ -50,10 +50,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.0.tgz#390ea202539c61c8fa6ba4428b57e05bc36dc47b" integrity sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ== -"@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@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== "@types/rimraf@^2.0.4": version "2.0.4" diff --git a/test/unit/browser/index.js b/test/unit/browser/index.js index 0b0a9e28a1..0908b1af9c 100644 --- a/test/unit/browser/index.js +++ b/test/unit/browser/index.js @@ -146,7 +146,7 @@ function consoleLogFn(msg) { } async function runTestsInBrowser(testModules, browserType) { - const args = process.platform === 'linux' && browserType === 'chromium' ? ['--no-sandbox'] : undefined; // disable sandbox to run chrome on certain Linux distros + const args = process.platform === 'linux' && browserType === 'chromium' ? ['--disable-setuid-sandbox'] : undefined; // setuid sandboxes requires root and is used in containers so we disable this to support our CI const browser = await playwright[browserType].launch({ headless: !Boolean(argv.debug), args }); const context = await browser.newContext(); const page = await context.newPage(); diff --git a/test/unit/browser/renderer.html b/test/unit/browser/renderer.html index 187ed60d28..3f2f307f98 100644 --- a/test/unit/browser/renderer.html +++ b/test/unit/browser/renderer.html @@ -53,9 +53,10 @@ paths: { vs: new URL(`../../../${!!isBuild ? 'out-build' : 'out'}/vs`, baseUrl).href, assert: new URL('../assert.js', baseUrl).href, - sinon: new URL('../../../node_modules/sinon/pkg/sinon-1.17.7.js', baseUrl).href, + sinon: new URL('../../../node_modules/sinon/pkg/sinon.js', baseUrl).href, + 'sinon-test': new URL('../../../node_modules/sinon-test/dist/sinon-test.js', baseUrl).href, xterm: new URL('../../../node_modules/xterm/lib/xterm.js', baseUrl).href, - sql: new URL(`../../../${!!isBuild ? 'out-build' : 'out'}/sql`, baseUrl).href, + sql: new URL(`../../../${!!isBuild ? 'out-build' : 'out'}/sql`, baseUrl).href, // {{SQL CARBON EDIT}} 'iconv-lite-umd': new URL('../../../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js', baseUrl).href, jschardet: new URL('../../../node_modules/jschardet/dist/jschardet.min.js', baseUrl).href } diff --git a/test/unit/electron/index.js b/test/unit/electron/index.js index fe834dc0ee..74d3b4fc00 100644 --- a/test/unit/electron/index.js +++ b/test/unit/electron/index.js @@ -25,7 +25,7 @@ app.allowRendererProcessReuse = false; const optimist = require('optimist') .describe('grep', 'only run tests matching ').alias('grep', 'g').alias('grep', 'f').string('grep') - .describe('invert', 'uses the inverse of the match specified by grep').alias('invert', 'i').string('invert') + .describe('invert', 'uses the inverse of the match specified by grep').alias('invert', 'i').string('invert') // {{SQL CARBON EDIT}} .describe('run', 'only run tests from ').string('run') .describe('runGlob', 'only run tests matching ').alias('runGlob', 'glob').alias('runGlob', 'runGrep').string('runGlob') .describe('build', 'run with build output (out-build)').boolean('build') @@ -177,7 +177,6 @@ app.on('ready', () => { nodeIntegration: true, contextIsolation: false, enableWebSQL: false, - enableRemoteModule: false, spellcheck: false, nativeWindowOpen: true, webviewTag: true diff --git a/test/unit/electron/renderer.js b/test/unit/electron/renderer.js index fd8bdc458e..b04b059601 100644 --- a/test/unit/electron/renderer.js +++ b/test/unit/electron/renderer.js @@ -176,6 +176,7 @@ function loadTests(opts) { // collect unexpected errors loader.require(['vs/base/common/errors'], function (errors) { + // {{SQL CARBON EDIT}} global.window.addEventListener('unhandledrejection', event => { errors.onUnexpectedError(event.reason); event.preventDefault(); @@ -297,6 +298,7 @@ function runTests(opts) { return loadTests(opts).then(() => { if (opts.grep) { + // {{SQL CARBON EDIT}} mocha.grep(new RegExp(opts.grep)); if (opts.invert) { mocha.invert(); diff --git a/test/unit/node/all.js b/test/unit/node/all.js index d3ca93d04e..29214c98ad 100644 --- a/test/unit/node/all.js +++ b/test/unit/node/all.js @@ -170,6 +170,7 @@ function main() { // replace the default unexpected error handler to be useful during tests loader(['vs/base/common/errors'], function (errors) { + // {{SQL CARBON EDIT}} global.window.addEventListener('unhandledrejection', event => { errors.onUnexpectedError(event.reason); event.preventDefault(); diff --git a/test/unit/node/index.html b/test/unit/node/index.html index 55f9bb0f70..b0b65c6b2c 100644 --- a/test/unit/node/index.html +++ b/test/unit/node/index.html @@ -17,7 +17,8 @@ baseUrl: '/out', paths: { assert: '/test/unit/assert.js', - sinon: '/node_modules/sinon/pkg/sinon-1.17.7.js' + sinon: '/node_modules/sinon/pkg/sinon.js', + 'sinon-test': '/node_modules/sinon-test/dist/sinon-test.js' } }); diff --git a/yarn.lock b/yarn.lock index 8082a553fd..7ed7c55ce8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -373,6 +373,34 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== +"@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" + integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^7.0.4", "@sinonjs/fake-timers@^7.1.0": + version "7.1.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz#2524eae70c4910edccf99b2f4e6efc5894aff7b5" + integrity sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@sinonjs/samsam@^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-6.0.2.tgz#a0117d823260f282c04bff5f8704bdc2ac6910bb" + integrity sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ== + dependencies: + "@sinonjs/commons" "^1.6.0" + lodash.get "^4.4.2" + type-detect "^4.0.8" + +"@sinonjs/text-encoding@^0.7.1": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" + integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== + "@szmarczak/http-timer@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" @@ -385,14 +413,15 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== -"@ts-morph/common@~0.9.0": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.9.0.tgz#a306355bad82cff22a1881f7f2f2c710bbb4d69d" - integrity sha512-yPcW6koNVK1hVKUu+KhPzhfgMb0uwzr2FewF+q8kxLerl0b+YZwmjvFMU2qbIawytIHT2VBI4bi+C09EFPB4aw== +"@ts-morph/common@~0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.10.0.tgz#d032ea6f6d4115b72fa50ad56009baebcc1e71b8" + integrity sha512-6wC+CovwzxLP+bQZcqHJEbZ7ViaIfsid8VzsVjJRkdfCQ8C8K5mm1+9/wkgmn814BPATtgSgFuDmVJnIb8/leg== dependencies: fast-glob "^3.2.5" minimatch "^3.0.4" mkdirp "^1.0.4" + path-browserify "^1.0.1" "@types/anymatch@*": version "1.3.1" @@ -536,6 +565,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-4.2.22.tgz#cf488a0f6b4a9c245d09927f4f757ca278b9c8ce" integrity sha512-LXRap3bb4AjtLZ5NOFc4ssVZrQPTgdPcNm++0SEJuJZaOA+xHkojJNYqy33A5q/94BmG5tA6yaMeD4VdCv5aSA== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== + "@types/node@>= 8": version "14.14.22" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.22.tgz#0d29f382472c4ccf3bd96ff0ce47daf5b7b84b18" @@ -546,7 +580,7 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.21.tgz#7e8a0c34cf29f4e17a36e9bd0ea72d45ba03908e" integrity sha512-CBgLNk4o3XMnqMc0rhb6lc77IwShMEglz05deDcn2lQxyXEZivfwgYJu7SMha9V5XcrP6qZuevTHV/QrN2vjKQ== -"@types/node@^14.14.37", "@types/node@^14.6.2": +"@types/node@^14.6.2": version "14.14.37" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.37.tgz#a3dd8da4eb84a996c36e331df98d82abd76b516e" integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw== @@ -575,10 +609,19 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45" integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ== -"@types/sinon@^1.16.36": - version "1.16.36" - resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-1.16.36.tgz#74bb6ed7928597c1b3fb1b009005e94dc6eae357" - integrity sha1-dLtu15KFl8Gz+xsAkAXpTcbq41c= +"@types/sinon-test@^2.4.2": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@types/sinon-test/-/sinon-test-2.4.2.tgz#f55bdf5486e7b7a4dd7257789fcc2b7b125c4164" + integrity sha512-3BX9mk5+o//Xzs5N4bFYxPT+QlPLrqbyNfDWkIGtk9pVIp2Nl8ctsIGXsY3F01DsCd1Zlin3FqAk6V5XqkCyJA== + dependencies: + "@types/sinon" "*" + +"@types/sinon@*", "@types/sinon@^10.0.2": + version "10.0.2" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.2.tgz#f360d2f189c0fd433d14aeb97b9d705d7e4cc0e4" + integrity sha512-BHn8Bpkapj8Wdfxvh2jWIUoaYB/9/XhsL0oOvBfRagJtKlSl9NWPcFOz2lRukI9szwGxFtYZCTejJSqsGDbdmw== + dependencies: + "@sinonjs/fake-timers" "^7.1.0" "@types/source-list-map@*": version "0.1.2" @@ -3177,7 +3220,7 @@ diagnostic-channel@0.2.0: dependencies: semver "^5.3.0" -diff@5.0.0: +diff@5.0.0, diff@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== @@ -3345,9 +3388,9 @@ editorconfig@^0.15.2: sigmund "^1.0.1" electron-to-chromium@^1.3.723: - version "1.3.749" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.749.tgz#0ecebc529ceb49dd2a7c838ae425236644c3439a" - integrity sha512-F+v2zxZgw/fMwPz/VUGIggG4ZndDsYy0vlpthi3tjmDZlcfbhN5mYW0evXUsBr2sUtuDANFtle410A9u/sd/4A== + version "1.3.737" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.737.tgz#196f2e9656f4f3c31930750e1899c091b72d36b5" + integrity sha512-P/B84AgUSQXaum7a8m11HUsYL8tj9h/Pt5f7Hg7Ty6bm5DxlFq+e5+ouHUoNQMsKDJ7u4yGfI8mOErCmSH9wyg== electron@12.0.7: version "12.0.7" @@ -4339,13 +4382,6 @@ form-data@~2.3.2: combined-stream "1.0.6" mime-types "^2.1.12" -formatio@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.1.1.tgz#5ed3ccd636551097383465d996199100e86161e9" - integrity sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek= - dependencies: - samsam "~1.1" - fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -4806,10 +4842,10 @@ got@^9.6.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" -graceful-fs@4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" - integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== +graceful-fs@4.2.6, graceful-fs@^4.2.4: + version "4.2.6" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" + integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.4" @@ -6128,9 +6164,9 @@ istextorbinary@1.0.2: textextensions "~1.0.0" jpeg-js@^0.4.2: - version "0.4.2" - resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.2.tgz#8b345b1ae4abde64c2da2fe67ea216a114ac279d" - integrity sha512-+az2gi/hvex7eLTMTlbRLOhH6P6WFdk2ITI8HJsaH2VqYO0I594zXSYEP+tf4FW+8Cy68ScDXoAsQdyQanv3sw== + version "0.4.3" + resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.3.tgz#6158e09f1983ad773813704be80680550eff977b" + integrity sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q== jquery@3.5.0: version "3.5.0" @@ -6181,10 +6217,10 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= -jschardet@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-2.3.0.tgz#06e2636e16c8ada36feebbdc08aa34e6a9b3ff75" - integrity sha512-6I6xT7XN/7sBB7q8ObzKbmv5vN+blzLcboDE1BNEsEfmRXJValMxO6OIRT69ylPBRemS3rw6US+CMCar0OBc9g== +jschardet@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882" + integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ== jsdoctypeparser@^6.1.0: version "6.1.0" @@ -6318,6 +6354,11 @@ just-debounce@^1.0.0: resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.0.0.tgz#87fccfaeffc0b68cd19d55f6722943f929ea35ea" integrity sha1-h/zPrv/AtozRnVX2cilD+SnqNeo= +just-extend@^4.0.2: + version "4.2.1" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744" + integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg== + keytar@7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.2.0.tgz#4db2bec4f9700743ffd9eda22eebb658965c8440" @@ -6541,6 +6582,11 @@ lodash.escaperegexp@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c= +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + lodash.isequal@^4.0.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" @@ -6603,11 +6649,6 @@ log-symbols@4.0.0: dependencies: chalk "^4.0.0" -lolex@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.3.2.tgz#7c3da62ffcb30f0f5a80a2566ca24e45d8a01f31" - integrity sha1-fD2mL/yzDw9agKJWbKJORdigHzE= - lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" @@ -6921,9 +6962,9 @@ mime@^1.4.1: integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== mime@^2.4.6: - version "2.4.6" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" - integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== + version "2.5.2" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" + integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== mimic-fn@^1.0.0: version "1.1.0" @@ -7052,7 +7093,7 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^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 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.0, mkdirp@~0.5.1: +"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@^0.5.5, mkdirp@~0.5.0, 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== @@ -7279,6 +7320,17 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4" integrity sha512-2NpiFHqC87y/zFke0fC0spBXL3bBsoh/p5H1EFhshxjCR5+0g2d6BiXbUFz9v1sAcxsk2htp2eQnNIci2dIYcA== +nise@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.0.tgz#713ef3ed138252daef20ec035ab62b7a28be645c" + integrity sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ== + dependencies: + "@sinonjs/commons" "^1.7.0" + "@sinonjs/fake-timers" "^7.0.4" + "@sinonjs/text-encoding" "^0.7.1" + just-extend "^4.0.2" + path-to-regexp "^1.7.0" + node-abi@^2.21.0: version "2.21.0" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.21.0.tgz#c2dc9ebad6f4f53d6ea9b531e7b8faad81041d48" @@ -7341,10 +7393,10 @@ node-pre-gyp@^0.10.0: semver "^5.3.0" tar "^4" -node-pty@0.10.1: - version "0.10.1" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.10.1.tgz#cd05d03a2710315ec40221232ec04186f6ac2c6d" - integrity sha512-JTdtUS0Im/yRsWJSx7yiW9rtpfmxqxolrtnyKwPLI+6XqTAPW/O2MjS8FYL4I5TsMbH2lVgDb2VMjp+9LoQGNg== +node-pty@0.11.0-beta7: + version "0.11.0-beta7" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.11.0-beta7.tgz#aed0888b5032d96c54d8473455e6adfae3bbebbe" + integrity sha512-uApPGLglZRiHQcUMWakbZOrBo8HVWvhzIqNnrWvBGJOvc6m/S5lCdbbg93BURyJqHFmBS0GV+4hwiMNDuGRbSA== dependencies: nan "^2.14.0" @@ -7902,6 +7954,11 @@ path-browserify@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-browserify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + path-dirname@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" @@ -7956,6 +8013,13 @@ path-root@^0.1.1: dependencies: path-root-regex "^0.1.0" +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -8533,11 +8597,11 @@ promise-inflight@^1.0.1: integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= proper-lockfile@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.1.tgz#284cf9db9e30a90e647afad69deb7cb06881262c" - integrity sha512-1w6rxXodisVpn7QYvLk706mzprPTAPCYAqxMvctmPN3ekuRk/kuGkGc82pangZiAt4R3lwSuUzheTTn0/Yb7Zg== + version "4.1.2" + resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f" + integrity sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA== dependencies: - graceful-fs "^4.1.11" + graceful-fs "^4.2.4" retry "^0.12.0" signal-exit "^3.0.2" @@ -9287,16 +9351,6 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -samsam@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567" - integrity sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc= - -samsam@~1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.3.tgz#9f5087419b4d091f232571e7fa52e90b0f552621" - integrity sha1-n1CHQZtNCR8jJXHn+lLpCw9VJiE= - sanitize-filename@^1.6.2: version "1.6.3" resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378" @@ -9510,15 +9564,22 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" -sinon@^1.17.2: - version "1.17.7" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-1.17.7.tgz#4542a4f49ba0c45c05eb2e9dd9d203e2b8efe0bf" - integrity sha1-RUKk9JugxFwF6y6d2dID4rjv4L8= +sinon-test@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/sinon-test/-/sinon-test-3.1.0.tgz#25a3f4d9a9deb172252407041d577d67b73fefd5" + integrity sha512-aGQwq6Xl9eJg/8Ugv4Ko4LQWUqjwRYNI8UtxnKa9hmcMEz3HBTR3nnzYrbW4isuRLsJWFuJTUcPGuz7f4XvODg== + +sinon@^11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-11.1.1.tgz#99a295a8b6f0fadbbb7e004076f3ae54fc6eab91" + integrity sha512-ZSSmlkSyhUWbkF01Z9tEbxZLF/5tRC9eojCdFh33gtQaP7ITQVaMWQHGuFM7Cuf/KEfihuh1tTl3/ABju3AQMg== dependencies: - formatio "1.1.1" - lolex "1.3.2" - samsam "1.1.2" - util ">=0.10.3 <1" + "@sinonjs/commons" "^1.8.3" + "@sinonjs/fake-timers" "^7.1.0" + "@sinonjs/samsam" "^6.0.2" + diff "^5.0.0" + nise "^5.1.0" + supports-color "^7.2.0" slash@^3.0.0: version "3.0.0" @@ -9666,13 +9727,13 @@ spawn-command@^0.0.2-1: resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" integrity sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A= -spdlog@^0.11.1: - version "0.11.1" - resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.11.1.tgz#29721b31018a5fe6a3ce2531f9d8d43e0bd6b825" - integrity sha512-M+sg9/Tnr0lrfnW2/hqgpoc4Z8Jzq7W8NUn35iiSslj+1uj1pgutI60MCpulDP2QyFzOpC8VsJmYD6Fub7wHoA== +spdlog@^0.13.0: + version "0.13.5" + resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.13.5.tgz#a31027dcccbe032e9a53579f42cb45428af08bad" + integrity sha512-D1xA5tRXw7eZOoFBCAnOxCxLN3JpHVDjpPJG/xjJ0nFZvtfOUTAzK66MVxJCDht/ZFwjLcBAltvzjfz4JTuSEw== dependencies: bindings "^1.5.0" - mkdirp "^0.5.1" + mkdirp "^0.5.5" nan "^2.14.0" spdx-correct@~1.0.0: @@ -10107,6 +10168,13 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-color@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + sver-compat@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/sver-compat/-/sver-compat-1.5.0.tgz#3cf87dfeb4d07b4a3f14827bc186b3fd0c645cd8" @@ -10481,12 +10549,12 @@ ts-loader@^6.2.1: micromatch "^4.0.0" semver "^6.0.0" -ts-morph@^10.0.2: - version "10.0.2" - resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-10.0.2.tgz#292418207db467326231b2be92828b5e295e7946" - integrity sha512-TVuIfEqtr9dW25K3Jajqpqx7t/zLRFxKu2rXQZSDjTm4MO4lfmuj1hn8WEryjeDDBFcNOCi+yOmYUYR4HucrAg== +ts-morph@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-11.0.0.tgz#511b3caa194739fef0619367f8e65de9b475e1d4" + integrity sha512-u5y0jaft5c0sRFnU0K8cZhhsvPUtXjZK5L31JLIhP17qcqo9MDjwsSYLg3UryQDzlktv8wyf/UtoqpFLDYHijg== dependencies: - "@ts-morph/common" "~0.9.0" + "@ts-morph/common" "~0.10.0" code-block-writer "^10.1.1" tsec@0.1.4: @@ -10550,6 +10618,11 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" +type-detect@4.0.8, type-detect@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + type-fest@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" @@ -10595,10 +10668,10 @@ typescript@^2.6.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" integrity sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q= -typescript@^4.3.0-dev.20210426: - version "4.3.0-dev.20210426" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.0-dev.20210426.tgz#00198cb8828f6a04b4e0ae32554a486bf7137a53" - integrity sha512-8YTqlzf3w8O8XwnnRlwRV2rswu7V7WEPUnAnH1BPPMrr06thNByMjIadA5SDW3tUJc1MG8Uj3NgZYocU5fWTVg== +typescript@^4.4.0-dev.20210607: + version "4.4.0-dev.20210607" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.0-dev.20210607.tgz#ea802e420785ef3b6b9c2e12d1ff4b8d2e52ee19" + integrity sha512-tKAp1IL4APSdxD7xHLDU6tIDOEN8yJOTUGG+cSdLunmysl3yOkGrdUbByDaFDmGjKywghGhQvcG8gOqbLUcDcg== typical@^4.0.0: version "4.0.0" @@ -10625,11 +10698,16 @@ unc-path-regex@^0.1.0, unc-path-regex@^0.1.2: resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= -underscore@^1.12.1, underscore@^1.7.0: +underscore@^1.12.1: version "1.12.1" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e" integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw== +underscore@^1.7.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1" + integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g== + underscore@~1.8.3: version "1.8.3" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" @@ -10782,7 +10860,7 @@ util.promisify@~1.0.0: has-symbols "^1.0.1" object.getownpropertydescriptors "^2.1.0" -util@0.10.3, "util@>=0.10.3 <1": +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= @@ -11007,10 +11085,10 @@ vscode-nls-dev@^3.3.1: xml2js "^0.4.19" yargs "^13.2.4" -vscode-oniguruma@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.3.1.tgz#e2383879c3485b19f533ec34efea9d7a2b14be8f" - integrity sha512-gz6ZBofA7UXafVA+m2Yt2zHKgXC2qedArprIsHAPKByTkwq9l5y/izAGckqxYml7mSbYxTRTfdRwsFq3cwF4LQ== +vscode-oniguruma@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.5.1.tgz#9ca10cd3ada128bd6380344ea28844243d11f695" + integrity sha512-JrBZH8DCC262TEYcYdeyZusiETu0Vli0xFgdRwNJjDcObcRjbmJP+IFcA3ScBwIXwgFHYKbAgfxtM/Cl+3Spjw== vscode-proxy-agent@^0.11.0: version "0.11.0" @@ -11032,14 +11110,6 @@ vscode-regexpp@^3.1.0: resolved "https://registry.yarnpkg.com/vscode-regexpp/-/vscode-regexpp-3.1.0.tgz#42d059b6fffe99bd42939c0d013f632f0cad823f" integrity sha512-pqtN65VC1jRLawfluX4Y80MMG0DHJydWhe5ZwMHewZD6sys4LbU6lHwFAHxeuaVE6Y6+xZOtAw+9hvq7/0ejkg== -vscode-ripgrep@^1.11.2: - version "1.11.2" - resolved "https://registry.yarnpkg.com/vscode-ripgrep/-/vscode-ripgrep-1.11.2.tgz#a1d9c717a20f625b7e14680cc7db25ffafd132d4" - integrity sha512-qMARNpPh/m6h9NbAQs4unGUnlAP2vrxt3a3nzbscrJcd5X9onoSdAYKG9vCkcxFJtOcQQm44a2Vf369mrrz8Sw== - dependencies: - https-proxy-agent "^4.0.0" - proxy-from-env "^1.1.0" - vscode-ripgrep@^1.11.3: version "1.11.3" resolved "https://registry.yarnpkg.com/vscode-ripgrep/-/vscode-ripgrep-1.11.3.tgz#a997f4f4535dfeb9d775f04053c1247454d7a37a" @@ -11055,19 +11125,19 @@ vscode-sqlite3@4.0.11: dependencies: nan "^2.14.0" -vscode-telemetry-extractor@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/vscode-telemetry-extractor/-/vscode-telemetry-extractor-1.7.0.tgz#f99b1a90a4cad0f75454f2f57615a155e55eb960" - integrity sha512-UC/N/uqPuQIuNnXg52XJnejeId2+Nuq04rj4H1rSZsqj9a56pigs6ogLPdZSi+OVLI21LU9PnJ/ZKrBrLm1roA== +vscode-telemetry-extractor@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/vscode-telemetry-extractor/-/vscode-telemetry-extractor-1.8.0.tgz#5562106fe2eebfce0593f336c91f5a5ddc154cee" + integrity sha512-jWe+caeLyB/F3V0EqsdkCC98wXx9+XLbm6EoPngz0sC4GOM7lcDSnVhUXzrIhZD/TSRPSPGlxp5r4/CrvhbmMQ== dependencies: command-line-args "^5.1.1" - ts-morph "^10.0.2" - vscode-ripgrep "^1.11.2" + ts-morph "^11.0.0" + vscode-ripgrep "^1.11.3" -vscode-textmate@5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e" - integrity sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ== +vscode-textmate@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.4.0.tgz#4b25ffc1f14ac3a90faf9a388c67a01d24257cd7" + integrity sha512-c0Q4zYZkcLizeYJ3hNyaVUM2AA8KDhNCA3JvXY8CeZSJuBdAy3bAvSbv46RClC4P3dSO9BdwhnKEx2zOo6vP/w== vscode-windows-ca-certs@^0.3.0: version "0.3.0" @@ -11222,15 +11292,15 @@ wide-align@1.1.3, wide-align@^1.1.0: dependencies: string-width "^1.0.2 || 2" -windows-foreground-love@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/windows-foreground-love/-/windows-foreground-love-0.2.0.tgz#b291832d8a02a966bc046ba0e498cc789809076b" - integrity sha512-72ZDshnt8Q3/ImLMt4wxsY8eVnUd1KDb5QfvZX09AxJJJa0hGdyzPfd/ms0pKSYYwKlEhB1ri+WDKNvdIpJknQ== +windows-foreground-love@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/windows-foreground-love/-/windows-foreground-love-0.4.0.tgz#79b628ba0ffc0436fa8066da8f85db042e431976" + integrity sha512-IPv60/Z6pJE8AQEBLzYWFfCVh6Z5G6qCrysbJzXYCKFkQY3XivsePdbZ0C0wqRNqsFjpVr06vnIdKfIcZFgDXQ== -windows-mutex@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/windows-mutex/-/windows-mutex-0.3.0.tgz#2f51a0c97b3979c98952b23c086035f1f3715fab" - integrity sha512-IDWzyHOEpQr7m590pT90jMbCYNe525d7BgP6F80TjispEu2gWMvTIoSuO6Sy4atIEhvs3ys7DVlKdLzIAyRviQ== +windows-mutex@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/windows-mutex/-/windows-mutex-0.4.1.tgz#2eccdfc312e15e7f212fb16280f060fc6b5f00cd" + integrity sha512-RW1xN6yzw8hAcfy1BKVClhJZg/PzuNz4Qz+CNhYmmdzJiwKSU9CqvVcmAvNrtdinNkXfSqTZVBVh5kUATg6xOA== dependencies: bindings "^1.2.1" nan "^2.14.0" @@ -11328,9 +11398,9 @@ ws@^3.3.3: ultron "~1.1.0" ws@^7.3.1: - version "7.4.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.0.tgz#a5dd76a24197940d4a8bb9e0e152bb4503764da7" - integrity sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ== + version "7.4.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== xml-name-validator@^1.0.0: version "1.0.0" @@ -11417,15 +11487,15 @@ xterm-addon-unicode11@0.3.0-beta.5: resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.3.0-beta.5.tgz#7e490799d530d3b301125c7a4e92317c161761c4" integrity sha512-SgDDL3PoMH1G48JO6T45whKAex4NPxi80UzUVitnrqyd8dFQP+oF6cxqUutULgm9HSGk62qy3mrZvIMGO5VXog== -xterm-addon-webgl@0.11.0-beta.8: - version "0.11.0-beta.8" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.11.0-beta.8.tgz#8cb4925d67c31beb8144275daf46358f42eff9fe" - integrity sha512-udRmQ/jgH8cL8VQOZweytkToIROevVeiA7WY0tIe878Wt2zKY+AYHZV8js3c1W9wHDu5G90BhmzTidJ5UwZK3Q== +xterm-addon-webgl@0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.11.1.tgz#33dd250ab52e9f51d2ff52396447962e6f53e24c" + integrity sha512-xF6DnEoV+rPtzetMBXBZVe1kLKtus7AKdEcyfq2eMHQzhaRvC+pfnU+XiCXC85kueguqu2UkBHXZs5mihK9jOQ== -xterm@4.12.0-beta.26: - version "4.12.0-beta.26" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.12.0-beta.26.tgz#57c75b732808795398a66bc1a3e06d09eaff2ada" - integrity sha512-yZB1kMBXQu2G0G1ch7TUi6f893iTZC+tmfjw/PQNZTmN46b4oX1l7rplc3sFcdrICHtmQ0Q5n1u0d6WUAdq1Kw== +xterm@4.13.0-beta.1: + version "4.13.0-beta.1" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.13.0-beta.1.tgz#ad2ad321c69a4add6e878c890f278a1da74fd7d0" + integrity sha512-gAMGqBglESTxQWph1uKVyd1jO/6eKsbicNG+Mr/YAsj06TjFVcLw839Iqu6P+DVFEV7lLLspcOb8fwX6qMBH/Q== y18n@^3.2.1: version "3.2.2"